aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts251
-rw-r--r--src/client/Network.ts33
-rw-r--r--src/client/apis/GoogleAuthenticationManager.scss3
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx114
-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/documents/DocumentTypes.ts2
-rw-r--r--src/client/documents/Documents.ts127
-rw-r--r--src/client/goldenLayout.js2
-rw-r--r--src/client/util/DictationManager.ts49
-rw-r--r--src/client/util/DocumentManager.ts171
-rw-r--r--src/client/util/DragManager.ts94
-rw-r--r--src/client/util/History.ts10
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.scss6
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx157
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts31
-rw-r--r--src/client/util/ParagraphNodeSpec.ts133
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts219
-rw-r--r--src/client/util/RichTextRules.ts184
-rw-r--r--src/client/util/RichTextSchema.tsx621
-rw-r--r--src/client/util/Scripting.ts8
-rw-r--r--src/client/util/SearchUtil.ts48
-rw-r--r--src/client/util/SelectionManager.ts20
-rw-r--r--src/client/util/SharingManager.scss140
-rw-r--r--src/client/util/SharingManager.tsx301
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx20
-rw-r--r--src/client/util/TooltipTextMenu.tsx164
-rw-r--r--src/client/util/UndoManager.ts2
-rw-r--r--src/client/util/clamp.js15
-rw-r--r--src/client/util/convertToCSSPTValue.js43
-rw-r--r--src/client/util/prosemirrorPatches.js139
-rw-r--r--src/client/util/toCSSLineSpacing.js64
-rw-r--r--src/client/views/ContextMenu.scss4
-rw-r--r--src/client/views/ContextMenu.tsx5
-rw-r--r--src/client/views/ContextMenuItem.tsx12
-rw-r--r--src/client/views/DocumentButtonBar.scss129
-rw-r--r--src/client/views/DocumentButtonBar.tsx367
-rw-r--r--src/client/views/DocumentDecorations.scss35
-rw-r--r--src/client/views/DocumentDecorations.tsx518
-rw-r--r--src/client/views/EditableView.tsx5
-rw-r--r--src/client/views/GlobalKeyHandler.ts13
-rw-r--r--src/client/views/InkingCanvas.scss5
-rw-r--r--src/client/views/InkingCanvas.tsx6
-rw-r--r--src/client/views/InkingControl.tsx41
-rw-r--r--src/client/views/Main.scss47
-rw-r--r--src/client/views/Main.tsx20
-rw-r--r--src/client/views/MainOverlayTextBox.scss29
-rw-r--r--src/client/views/MainOverlayTextBox.tsx159
-rw-r--r--src/client/views/MainView.tsx375
-rw-r--r--src/client/views/MainViewModal.scss25
-rw-r--r--src/client/views/MainViewModal.tsx44
-rw-r--r--src/client/views/MetadataEntryMenu.tsx2
-rw-r--r--src/client/views/OverlayView.scss5
-rw-r--r--src/client/views/OverlayView.tsx79
-rw-r--r--src/client/views/PreviewCursor.tsx10
-rw-r--r--src/client/views/ScriptBox.tsx42
-rw-r--r--src/client/views/ScriptingRepl.scss1
-rw-r--r--src/client/views/ScriptingRepl.tsx20
-rw-r--r--src/client/views/SearchBox.tsx183
-rw-r--r--src/client/views/SearchItem.tsx67
-rw-r--r--src/client/views/TemplateMenu.tsx87
-rw-r--r--src/client/views/Templates.tsx38
-rw-r--r--src/client/views/collections/CollectionBaseView.scss18
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx59
-rw-r--r--src/client/views/collections/CollectionDockingView.scss5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx231
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx5
-rw-r--r--src/client/views/collections/CollectionPDFView.scss47
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx24
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx25
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx8
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx80
-rw-r--r--src/client/views/collections/CollectionStackingView.scss2
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx62
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx82
-rw-r--r--src/client/views/collections/CollectionSubView.tsx100
-rw-r--r--src/client/views/collections/CollectionTreeView.scss19
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx145
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx6
-rw-r--r--src/client/views/collections/CollectionView.tsx58
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx190
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss9
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx50
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx117
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx9
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx14
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx932
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx154
-rw-r--r--src/client/views/document_templates/image_card/ImageCard.tsx3
-rw-r--r--src/client/views/linking/LinkEditor.scss (renamed from src/client/views/nodes/LinkEditor.scss)0
-rw-r--r--src/client/views/linking/LinkEditor.tsx (renamed from src/client/views/nodes/LinkEditor.tsx)0
-rw-r--r--src/client/views/linking/LinkFollowBox.scss93
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx571
-rw-r--r--src/client/views/linking/LinkMenu.scss (renamed from src/client/views/nodes/LinkMenu.scss)0
-rw-r--r--src/client/views/linking/LinkMenu.tsx (renamed from src/client/views/nodes/LinkMenu.tsx)5
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx (renamed from src/client/views/nodes/LinkMenuGroup.tsx)36
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx (renamed from src/client/views/nodes/LinkMenuItem.tsx)88
-rw-r--r--src/client/views/nodes/Annotation.tsx117
-rw-r--r--src/client/views/nodes/ButtonBox.tsx25
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss5
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx137
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx41
-rw-r--r--src/client/views/nodes/DocumentView.scss58
-rw-r--r--src/client/views/nodes/DocumentView.tsx894
-rw-r--r--src/client/views/nodes/DragBox.tsx10
-rw-r--r--src/client/views/nodes/FieldView.tsx9
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss121
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx956
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.scss34
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx154
-rw-r--r--src/client/views/nodes/IconBox.tsx58
-rw-r--r--src/client/views/nodes/ImageBox.scss120
-rw-r--r--src/client/views/nodes/ImageBox.tsx94
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx33
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx13
-rw-r--r--src/client/views/nodes/PDFBox.scss161
-rw-r--r--src/client/views/nodes/PDFBox.tsx289
-rw-r--r--src/client/views/nodes/PresBox.scss33
-rw-r--r--src/client/views/nodes/PresBox.tsx531
-rw-r--r--src/client/views/nodes/VideoBox.tsx82
-rw-r--r--src/client/views/nodes/WebBox.scss1
-rw-r--r--src/client/views/nodes/WebBox.tsx25
-rw-r--r--src/client/views/pdf/Annotation.scss3
-rw-r--r--src/client/views/pdf/Annotation.tsx60
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.scss6
-rw-r--r--src/client/views/pdf/PDFAnnotationLayer.tsx21
-rw-r--r--src/client/views/pdf/PDFMenu.tsx17
-rw-r--r--src/client/views/pdf/PDFViewer.scss138
-rw-r--r--src/client/views/pdf/PDFViewer.tsx779
-rw-r--r--src/client/views/pdf/Page.scss31
-rw-r--r--src/client/views/pdf/Page.tsx292
-rw-r--r--src/client/views/presentationview/PresElementBox.scss84
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx225
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx425
-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.tsx100
-rw-r--r--src/client/views/presentationview/PresentationView.scss113
-rw-r--r--src/client/views/search/FilterBox.tsx6
-rw-r--r--src/client/views/search/SearchBox.scss9
-rw-r--r--src/client/views/search/SearchBox.tsx63
-rw-r--r--src/client/views/search/SearchItem.scss13
-rw-r--r--src/client/views/search/SearchItem.tsx98
-rw-r--r--src/debug/Repl.tsx8
-rw-r--r--src/debug/Test.tsx35
-rw-r--r--src/extensions/ArrayExtensions.ts13
-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.ts296
-rw-r--r--src/new_fields/InkField.ts2
-rw-r--r--src/new_fields/RichTextField.ts49
-rw-r--r--src/new_fields/RichTextUtils.ts519
-rw-r--r--src/new_fields/ScriptField.ts36
-rw-r--r--src/scraping/buxton/scraper.py1
-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/apis/google/GoogleApiServerUtils.ts194
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts85
-rw-r--r--src/server/apis/google/SharedTypes.ts21
-rw-r--r--src/server/authentication/config/passport.ts3
-rw-r--r--src/server/authentication/models/current_user_utils.ts105
-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.ts429
-rw-r--r--src/server/index.ts410
-rw-r--r--src/server/updateSearch.ts123
-rw-r--r--src/typings/index.d.ts632
173 files changed, 11744 insertions, 7525 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 959b89fe5..9a2f01f80 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,14 +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}`);
+ }
+
+ export function shareUrl(documentId: string): string {
+ return prepend(`/doc/${documentId}?sharing=true`);
+ }
+
+ export function CorsProxy(url: string): string {
+ return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
}
- public static CopyText(text: string) {
+ export function CopyText(text: string) {
var textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
@@ -53,7 +61,95 @@ export class Utils {
document.body.removeChild(textArea);
}
- public static GetClipboardText(): string {
+ 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 g = gm ? Number(gm[1]) : 0;
+ 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;
+ return { r: r, g: g, b: b, a: a };
+ }
+
+ 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 : "") + ")";
+ }
+
+ export function HSLtoRGB(h: number, s: number, l: number) {
+ // Must be fractions of 1
+ // s /= 100;
+ // l /= 100;
+
+ let c = (1 - Math.abs(2 * l - 1)) * s,
+ x = c * (1 - Math.abs((h / 60) % 2 - 1)),
+ m = l - c / 2,
+ r = 0,
+ g = 0,
+ b = 0;
+ if (0 <= h && h < 60) {
+ r = c; g = x; b = 0;
+ } else if (60 <= h && h < 120) {
+ r = x; g = c; b = 0;
+ } else if (120 <= h && h < 180) {
+ r = 0; g = c; b = x;
+ } else if (180 <= h && h < 240) {
+ r = 0; g = x; b = c;
+ } else if (240 <= h && h < 300) {
+ r = x; g = 0; b = c;
+ } else if (300 <= h && h < 360) {
+ r = c; g = 0; b = x;
+ }
+ r = Math.round((r + m) * 255);
+ g = Math.round((g + m) * 255);
+ b = Math.round((b + m) * 255);
+ return { r: r, g: g, b: b };
+ }
+
+ export function RGBToHSL(r: number, g: number, b: number) {
+ // Make r, g, and b fractions of 1
+ r /= 255;
+ g /= 255;
+ b /= 255;
+
+ // Find greatest and smallest channel values
+ let cmin = Math.min(r, g, b),
+ cmax = Math.max(r, g, b),
+ delta = cmax - cmin,
+ h = 0,
+ s = 0,
+ l = 0;
+ // Calculate hue
+
+ // No difference
+ if (delta === 0) h = 0;
+ // Red is max
+ else if (cmax === r) h = ((g - b) / delta) % 6;
+ // Green is max
+ else if (cmax === g) h = (b - r) / delta + 2;
+ // Blue is max
+ else h = (r - g) / delta + 4;
+
+ h = Math.round(h * 60);
+
+ // Make negative hues positive behind 360°
+ if (h < 0) h += 360; // Calculate lightness
+
+ l = (cmax + cmin) / 2;
+
+ // Calculate saturation
+ s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+
+ // Multiply l and s by 100
+ // s = +(s * 100).toFixed(1);
+ // l = +(l * 100).toFixed(1);
+
+ return { h: h, s: s, l: l };
+ }
+
+
+ export function GetClipboardText(): string {
var textArea = document.createElement("textarea");
document.body.appendChild(textArea);
textArea.focus();
@@ -66,51 +162,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)]);
});
}
}
@@ -133,6 +231,39 @@ export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => vo
return dup;
}
+export function timenow() {
+ var now = new Date();
+ let ampm = 'am';
+ let h = now.getHours();
+ let m: any = now.getMinutes();
+ let s: any = now.getSeconds();
+ if (h >= 12) {
+ if (h > 12) h -= 12;
+ ampm = 'pm';
+ }
+ if (m < 10) m = '0' + m;
+ return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
+}
+
+export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
+ return boundsList.reduce((bounds, b) => {
+ var [sptX, sptY] = [b.x, b.y];
+ let [bptX, bptY] = [sptX + b.width, sptY + b.height];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+}
+export function intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+}
+
+export function percent2frac(percent: string) {
+ return Number(percent.substr(0, percent.length - 1)) / 100;
+}
+
export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
export function returnTrue() { return true; }
@@ -178,12 +309,56 @@ export namespace JSONUtils {
}
-export function PostToServer(relativeRoute: string, body: any) {
- let options = {
- method: "POST",
- uri: Utils.prepend(relativeRoute),
- json: true,
- body: body
+const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
+ let newCurrentTime = currentTime / (duration / 2);
+
+ if (newCurrentTime < 1) {
+ return (change / 2) * newCurrentTime * newCurrentTime + start;
+ }
+
+ newCurrentTime -= 1;
+ return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
+};
+
+export function smoothScroll(duration: number, element: HTMLElement, to: number) {
+ const start = element.scrollTop;
+ const change = to - start;
+ const startDate = new Date().getTime();
+
+ const animateScroll = () => {
+ const currentDate = new Date().getTime();
+ const currentTime = currentDate - startDate;
+ element.scrollTop = easeInOutQuad(currentTime, start, change, duration);
+
+ if (currentTime < duration) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ element.scrollTop = to;
+ }
};
- return requestPromise.post(options);
+ 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..5efb3ab3b
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.scss
@@ -0,0 +1,3 @@
+.paste-target {
+ padding: 5px;
+} \ 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..db108ad94
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -0,0 +1,114 @@
+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;
+
+ 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(
+ token => {
+ runInAction(() => this.success = true);
+ setTimeout(() => {
+ this.isOpen = false;
+ runInAction(() => this.displayLauncher = false);
+ setTimeout(() => {
+ runInAction(() => this.success = undefined);
+ runInAction(() => this.displayLauncher = true);
+ this.hasBeenClicked = false;
+ }, 500);
+ }, 1000);
+ disposer();
+ resolve(token);
+ },
+ () => {
+ this.hasBeenClicked = false;
+ runInAction(() => this.success = false);
+ }
+ );
+ }
+ }
+ );
+ });
+ }
+ // otherwise, we already have a valid, stored access token
+ return response;
+ }
+
+ 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 style={{ display: "flex", flexDirection: "column" }}>
+ {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)}
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ overlayDisplayedOpacity={0.9}
+ dialogueBoxStyle={{ borderColor: this.success === undefined ? "black" : this.success ? "green" : "red" }}
+ />
+ );
+ }
+
+} \ 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/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 1578e49fe..e5d5885cd 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -19,4 +19,6 @@ export enum DocumentType {
YOUTUBE = "youtube",
DRAGBOX = "dragbox",
PRES = "presentation",
+ LINKFOLLOW = "linkfollow",
+ PRESELEMENT = "preselement"
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 47df17329..9d1a6ed3e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,6 @@
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";
@@ -20,8 +19,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 } 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";
@@ -45,14 +44,15 @@ import { PresBox } from "../views/nodes/PresBox";
import { ComputedField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
-//import { PresBox } from "../views/nodes/PresBox";
-//import { PresField } from "../../new_fields/PresField";
+import { LinkFollowBox } from "../views/linking/LinkFollowBox";
+import { PresElementBox } from "../views/presentationview/PresElementBox";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
export interface DocumentOptions {
x?: number;
y?: number;
+ z?: number;
type?: string;
width?: number;
height?: number;
@@ -63,7 +63,8 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
- layout?: string;
+ layout?: string | Doc;
+ isTemplate?: boolean;
templates?: List<string>;
viewType?: number;
backgroundColor?: string;
@@ -72,11 +73,14 @@ export interface DocumentOptions {
dropAction?: dropActionType;
backgroundLayout?: string;
chromeStatus?: string;
+ fontSize?: number;
curPage?: number;
+ currentTimecode?: number;
documentText?: string;
borderRounding?: string;
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
+ autoHeight?: boolean;
dbDoc?: Doc;
// [key: string]: Opt<Field>;
}
@@ -91,12 +95,13 @@ export namespace Docs {
export namespace Prototypes {
- type LayoutSource = { LayoutString: () => string };
+ type LayoutSource = { LayoutString: (ext?: string) => string };
type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
type CollectionViewType = [CollectionLayoutSource, string, string?];
type PrototypeTemplate = {
layout: {
view: LayoutSource,
+ ext?: string, // optional extension field for layout source
collectionView?: CollectionViewType
},
options?: Partial<DocumentOptions>
@@ -117,7 +122,7 @@ export namespace Docs {
}],
[DocumentType.IMG, {
layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
- options: { nativeWidth: 600, curPage: 0 }
+ options: {}
}],
[DocumentType.WEB, {
layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
@@ -133,14 +138,14 @@ export namespace Docs {
}],
[DocumentType.VID, {
layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
- options: { nativeWidth: 600, curPage: 0 },
+ options: { currentTimecode: 0 },
}],
[DocumentType.AUDIO, {
layout: { view: AudioBox },
options: { height: 32 }
}],
[DocumentType.PDF, {
- layout: { view: PDFBox, collectionView: [CollectionPDFView, data, anno] as CollectionViewType },
+ layout: { view: PDFBox, ext: anno },
options: { nativeWidth: 1200, curPage: 1 }
}],
[DocumentType.ICON, {
@@ -154,7 +159,6 @@ export namespace Docs {
[DocumentType.LINKDOC, {
data: new List<Doc>(),
layout: { view: EmptyBox },
- options: {}
}],
[DocumentType.YOUTUBE, {
layout: { view: YoutubeBox }
@@ -169,7 +173,13 @@ export namespace Docs {
[DocumentType.DRAGBOX, {
layout: { view: DragBox },
options: { width: 40, height: 40 },
- }]
+ }],
+ [DocumentType.LINKFOLLOW, {
+ layout: { view: LinkFollowBox }
+ }],
+ [DocumentType.PRESELEMENT, {
+ layout: { view: PresElementBox }
+ }],
]);
// All document prototypes are initialized with at least these values
@@ -246,8 +256,8 @@ 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();
+ let options = { title, 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]);
@@ -326,7 +336,13 @@ 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) {
@@ -336,6 +352,7 @@ export namespace Docs {
inst.proto!.height = NumCast(inst.proto!.width) * aspect;
})
.catch((err: any) => console.log(err));
+ // }
return inst;
}
export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) {
@@ -413,8 +430,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
}
- export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform });
+ export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
}
export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
@@ -442,6 +459,14 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), 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);
}
@@ -496,10 +521,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);
@@ -565,6 +593,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;
@@ -596,35 +625,61 @@ export namespace Docs {
export namespace DocUtils {
- export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc) {
- if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
- let sv = DocumentManager.Instance.getDocumentView(source);
- if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
- if (target === CurrentUserUtils.UserDocument) return undefined;
+ export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
+ targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, "");
+ DocServer.GetRefField(targetID).then(doc => {
+ if (promoteDoc !== doc) {
+ let copy = doc as Doc;
+ if (copy) {
+ Doc.Overwrite(promoteDoc, copy, true);
+ } else {
+ copy = Doc.MakeCopy(promoteDoc, true, targetID);
+ }
+ !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID);
+ addDoc && addDoc(copy);
+ remDoc && remDoc(promoteDoc);
+ if (!doc) {
+ DocListCastAsync(promoteDoc.links).then(links => {
+ links && links.map(async link => {
+ if (link) {
+ let a1 = await Cast(link.anchor1, Doc);
+ if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
+ let a2 = await Cast(link.anchor2, Doc);
+ if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
+ LinkManager.Instance.deleteLink(link);
+ LinkManager.Instance.addLink(link);
+ }
+ });
+ });
+ }
+ }
+ });
+ }
+ export function 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();
+ 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.type = DocumentType.LINK;
- linkDocProto.anchor1 = source;
- linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1 = source.doc;
+ linkDocProto.anchor1Context = source.ctx;
+ linkDocProto.anchor1Timecode = source.doc.currentTimecode;
linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor2 = target;
- linkDocProto.anchor2Page = target.curPage;
+ linkDocProto.anchor2 = target.doc;
+ linkDocProto.anchor2Context = target.ctx;
linkDocProto.anchor2Groups = new List<Doc>([]);
+ linkDocProto.anchor2Timecode = target.doc.currentTimecode;
LinkManager.Instance.addLink(linkDocProto);
- let script = `return links(this);`;
- let computed = CompileScript(script, { params: { this: "Doc" }, typecheck: false });
- computed.compiled && (Doc.GetProto(source).links = new ComputedField(computed));
- computed.compiled && (Doc.GetProto(target).links = new ComputedField(computed));
+ 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/goldenLayout.js b/src/client/goldenLayout.js
index ad78139c1..29b750720 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -377,7 +377,7 @@
this._nOriginalY = coordinates.y;
this._oDocument.on('mousemove touchmove', this._fMove);
- this._oDocument.one('mouseup touchend', this._fUp);
+ this._oDocument.on('mouseup touchend', this._fUp);
this._timeout = setTimeout(lm.utils.fnBind(this._startDrag, this), this._nDelay);
}
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index fb3c15cea..3b2307073 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -3,7 +3,7 @@ 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";
@@ -40,12 +40,26 @@ 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 = " ... ";
@@ -55,8 +69,7 @@ export namespace DictationManager {
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;
@@ -109,6 +122,10 @@ export namespace DictationManager {
};
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;
@@ -167,14 +185,20 @@ 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;
}
isManuallyStopped = true;
@@ -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 {
@@ -313,7 +327,7 @@ export namespace DictationManager {
["open fields", {
action: (target: DocumentView) => {
let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
- target.props.addDocTab(kvp, target.dataDoc, "onRight");
+ target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
}
}],
@@ -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 124faf266..c45d3a75f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, trace } from 'mobx';
import { Doc, DocListCastAsync } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { Cast, NumCast } from '../../new_fields/Types';
+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';
@@ -10,6 +10,9 @@ 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 { notDeepEqual } from 'assert';
export class DocumentManager {
@@ -54,9 +57,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 | CollectionPDFView | CollectionVideoView): DocumentView | undefined {
- let toReturn: DocumentView | null = null;
+ let toReturn: DocumentView | undefined;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
for (let pass of passes) {
@@ -79,10 +82,14 @@ export class DocumentManager {
return toReturn;
}
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): 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[] = [];
@@ -126,82 +133,124 @@ export class DocumentManager {
}
- @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) {
- const page = NumCast(doc.page, linkPage || 0);
- const curPage = NumCast(contextDoc.curPage, page);
- if (page !== curPage) contextDoc.curPage = page;
- }
- 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;
- 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.Instance.AddRightSplit)(docContext, undefined);
- setTimeout(() => {
- this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
- }, 10);
- }
- } else {
- const actualDoc = Doc.MakeAlias(docDelegate);
- Doc.BrushDoc(actualDoc);
- if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.Instance.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.Instance.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);
- }, 10);
+ 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 doc = Doc.GetProto(docDelegate);
-
- let docView: DocumentView | null;
- docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
- docView.props.zoomToScale(scale);
- }
+ let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
+ docView && docView.props.zoomToScale(scale);
}
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 {
return 1;
}
}
+
+ @action
+ animateBetweenPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ expandedDocs && expandedDocs.map(expDoc => {
+ if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC
+ if (expDoc.isMinimized) { // docs are never actaully at the minimized location. so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location
+ expDoc.isMinimized = false;
+ expDoc.animateToPos = new List<number>([...scrpt, 0]);
+ expDoc.animateToDimensions = new List<number>([0, 0]);
+ }
+ setTimeout(() => {
+ expDoc.isAnimating = "max";
+ expDoc.animateToPos = new List<number>([0, 0, 1]);
+ expDoc.animateToDimensions = new List<number>([NumCast(expDoc.width), NumCast(expDoc.height)]);
+ setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600);
+ }, 0);
+ } else { // MINIMIZE DOC
+ expDoc.isAnimating = "min";
+ expDoc.animateToPos = new List<number>([...scrpt, 0]);
+ expDoc.animateToDimensions = new List<number>([0, 0]);
+ setTimeout(() => {
+ if (expDoc.isAnimating === "min") {
+ expDoc.isMinimized = true;
+ expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined;
+ }
+ }, 600);
+ }
+ });
+ }
}
-Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)) }) \ No newline at end of file
+Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 748fb9d13..ddc8fb62c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -32,7 +32,7 @@ export function SetupDrag(
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
let doc = await docFunc();
- var dragData = new DragManager.DocumentDragData([doc], [undefined]);
+ var dragData = new DragManager.DocumentDragData([doc]);
dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
dragData.options = options;
@@ -66,7 +66,7 @@ export function SetupDrag(
function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
const document = SelectionManager.SelectedDocuments()[0];
- document.props.removeDocument && document.props.removeDocument(doc);
+ document && document.props.removeDocument && document.props.removeDocument(doc);
addDocument(doc);
return true;
}
@@ -76,7 +76,7 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
if (draggeddoc) {
let moddrag = await Cast(draggeddoc.annotationOn, Doc);
let dragdocs = moddrag ? [moddrag] : [draggeddoc];
- let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ let dragData = new DragManager.DocumentDragData(dragdocs);
dragData.moveDocument = moveLinkedDocument;
DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
handlers: {
@@ -107,7 +107,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
if (doc) moddrag.push(doc);
}
let dragdocs = moddrag.length ? moddrag : draggedDocs;
- let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+ let dragData = new DragManager.DocumentDragData(dragdocs);
dragData.moveDocument = moveLinkedDocument;
DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
handlers: {
@@ -201,18 +201,14 @@ export namespace DragManager {
export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) {
+ constructor(dragDoc: Doc[]) {
this.draggedDocuments = dragDoc;
- this.draggedDataDocs = dragDataDocs;
this.droppedDocuments = dragDoc;
- this.xOffset = 0;
- this.yOffset = 0;
+ this.offset = [0, 0];
}
draggedDocuments: Doc[];
- draggedDataDocs: (Doc | undefined)[];
droppedDocuments: Doc[];
- xOffset: number;
- yOffset: number;
+ offset: number[];
dropAction: dropActionType;
userDropAction: dropActionType;
moveDocument?: MoveFunction;
@@ -225,14 +221,13 @@ export namespace DragManager {
this.dragDocument = dragDoc;
this.dropDocument = dropDoc;
this.annotationDocument = annotationDoc;
- this.xOffset = this.yOffset = 0;
+ this.offset = [0, 0];
}
targetContext: Doc | undefined;
dragDocument: Doc;
annotationDocument: Doc;
dropDocument: Doc;
- xOffset: number;
- yOffset: number;
+ offset: number[];
dropAction: dropActionType;
userDropAction: dropActionType;
}
@@ -252,22 +247,15 @@ export namespace DragManager {
});
}
- export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], downX: number, downY: number, options?: DragOptions) {
- let dragData = new DragManager.DocumentDragData([], [undefined]);
+ export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+ let dragData = new DragManager.DocumentDragData([]);
runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
(dropData: { [id: string]: any }) => {
let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
- let compiled = CompileScript(script, {
- params: { doc: Doc.name },
- typecheck: false,
- editable: true
- });
- if (compiled.compiled) {
- let scriptField = new ScriptField(compiled);
- bd.onClick = scriptField;
- }
+ bd.onClick = ScriptField.MakeScript(script);
params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
+ initialize && initialize(bd);
bd.buttonParams = new List<string>(params);
dropData.droppedDocuments = [bd];
});
@@ -282,7 +270,8 @@ export namespace DragManager {
let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
let dvs = DocumentManager.Instance.getDocumentViews(d);
if (dvs.length) {
- let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView);
+ let containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+ let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView);
if (inContext.length) {
inContext.forEach(dv => droppedDocs.push(dv.props.Document));
} else {
@@ -360,10 +349,8 @@ export namespace DragManager {
let xs: number[] = [];
let ys: number[] = [];
- const docs: Doc[] =
- dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
- const datadocs: (Doc | undefined)[] =
- dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
+ const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments :
+ dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
h = ele.offsetHeight;
@@ -392,22 +379,20 @@ export namespace DragManager {
dragElement.style.width = `${rect.width / scaleX}px`;
dragElement.style.height = `${rect.height / scaleY}px`;
- // bcz: if PDFs are rendered with svg's, then this code isn't needed
- // bcz: PDFs don't show up if you clone them when rendered using a canvas.
- // however, PDF's have a thumbnail field that contains an image of their canvas.
- // So we replace the pdf's canvas with the image thumbnail
- // if (docs.length) {
- // var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
- // let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField);
- // if (pdfBox && pdfBox.childElementCount && thumbnail) {
- // let img = new Image();
- // img.src = thumbnail.toString();
- // img.style.position = "absolute";
- // img.style.width = `${rect.width / scaleX}px`;
- // img.style.height = `${rect.height / scaleY}px`;
- // pdfBox.replaceChild(img, pdfBox.children[0])
- // }
- // }
+ if (docs.length) {
+ var pdfBox = dragElement.getElementsByTagName("canvas");
+ var pdfBoxSrc = ele.getElementsByTagName("canvas");
+ Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
+ var pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
+ var pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
+ let tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
+ let oldopacity = dragElement.style.opacity;
+ dragElement.style.opacity = "0";
+ setTimeout(() => {
+ dragElement.style.opacity = oldopacity;
+ Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] }));
+ }, 0);
+ }
let set = dragElement.getElementsByTagName('*');
if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
// tslint:disable-next-line: prefer-for-of
@@ -431,8 +416,8 @@ export namespace DragManager {
hideSource = options.hideSource();
}
}
- eles.map(ele => (ele.hidden = hideSource) &&
- (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = hideSource)));
+
+ eles.map(ele => ele.hidden = hideSource);
let lastX = downX;
let lastY = downY;
@@ -448,7 +433,7 @@ export namespace DragManager {
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- }, docs, datadocs);
+ }, docs);
}
//TODO: Why can't we use e.movementX and e.movementY?
let moveX = e.pageX - lastX;
@@ -460,12 +445,9 @@ export namespace DragManager {
);
};
- let hideDragElements = () => {
+ let hideDragShowOriginalElements = () => {
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.map(ele => {
- ele.hidden = false;
- (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = false));
- });
+ eles.map(ele => ele.hidden = false);
};
let endDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
@@ -476,12 +458,12 @@ export namespace DragManager {
};
AbortDrag = () => {
- hideDragElements();
+ hideDragShowOriginalElements();
SelectionManager.SetIsDragging(false);
endDrag();
};
const upHandler = (e: PointerEvent) => {
- hideDragElements();
+ hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
endDrag();
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index e9ff21b22..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;
@@ -52,7 +54,9 @@ export namespace HistoryUtil {
}
export function getState(): ParsedUrl {
- return copyState(history.state);
+ let state = copyState(history.state);
+ state.initializers = state.initializers || {};
+ return state;
}
// export function addHandler(handler: (state: ParsedUrl | null) => void) {
@@ -141,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 || {};
@@ -156,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 75b0b52a7..d74b51993 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,20 +17,35 @@ 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;
@@ -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 uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync(async batch => {
+ 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);
+ const responses = await Identified.PostFormDataToServer(RouteStore.upload, formData);
+ runInAction(() => this.completed += batch.length);
+ return responses as ImageUploadResponse[];
+ });
+
+ 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];
@@ -136,24 +160,39 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
};
let parent = this.props.ContainingCollectionView;
if (parent) {
- let importContainer = Docs.Create.StackingDocument(docs, options);
+ 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;
+ 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
@@ -188,7 +227,6 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
metadata.splice(index, 1);
}
}
-
}
}
@@ -196,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}
@@ -281,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",
@@ -313,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/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts
new file mode 100644
index 000000000..3a993e1ff
--- /dev/null
+++ b/src/client/util/ParagraphNodeSpec.ts
@@ -0,0 +1,133 @@
+import clamp from './clamp';
+import convertToCSSPTValue from './convertToCSSPTValue';
+import toCSSLineSpacing from './toCSSLineSpacing';
+import { Node, DOMOutputSpec } from 'prosemirror-model';
+
+//import type { NodeSpec } from './Types';
+type NodeSpec = {
+ attrs?: { [key: string]: any },
+ content?: string,
+ draggable?: boolean,
+ group?: string,
+ inline?: boolean,
+ name?: string,
+ parseDOM?: Array<any>,
+ toDOM?: (node: any) => DOMOutputSpec,
+};
+
+// This assumes that every 36pt maps to one indent level.
+export const INDENT_MARGIN_PT_SIZE = 36;
+export const MIN_INDENT_LEVEL = 0;
+export const MAX_INDENT_LEVEL = 7;
+export const ATTRIBUTE_INDENT = 'data-indent';
+
+export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']);
+
+const ALIGN_PATTERN = /(left|right|center|justify)/;
+
+// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
+// :: NodeSpec A plain paragraph textblock. Represented in the DOM
+// as a `<p>` element.
+const ParagraphNodeSpec: NodeSpec = {
+ attrs: {
+ align: { default: null },
+ color: { default: null },
+ id: { default: null },
+ indent: { default: null },
+ lineSpacing: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingBottom: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingTop: { default: null },
+ },
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p', getAttrs }],
+ toDOM,
+};
+
+function getAttrs(dom: HTMLElement): Object {
+ const {
+ lineHeight,
+ textAlign,
+ marginLeft,
+ paddingTop,
+ paddingBottom,
+ } = dom.style;
+
+ let align = dom.getAttribute('align') || textAlign || '';
+ align = ALIGN_PATTERN.test(align) ? align : "";
+
+ let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10);
+
+ if (!indent && marginLeft) {
+ indent = convertMarginLeftToIndentValue(marginLeft);
+ }
+
+ indent = indent || MIN_INDENT_LEVEL;
+
+ const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null;
+
+ const id = dom.getAttribute('id') || '';
+ return { align, indent, lineSpacing, paddingTop, paddingBottom, id };
+}
+
+function toDOM(node: Node): DOMOutputSpec {
+ const {
+ align,
+ indent,
+ lineSpacing,
+ paddingTop,
+ paddingBottom,
+ id,
+ } = node.attrs;
+ const attrs: { [key: string]: any } | null = {};
+
+ let style = '';
+ if (align && align !== 'left') {
+ style += `text-align: ${align};`;
+ }
+
+ if (lineSpacing) {
+ const cssLineSpacing = toCSSLineSpacing(lineSpacing);
+ style +=
+ `line-height: ${cssLineSpacing};` +
+ // This creates the local css variable `--czi-content-line-height`
+ // that its children may apply.
+ `--czi-content-line-height: ${cssLineSpacing}`;
+ }
+
+ if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) {
+ style += `padding-top: ${paddingTop};`;
+ }
+
+ if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) {
+ style += `padding-bottom: ${paddingBottom};`;
+ }
+
+ style && (attrs.style = style);
+
+ if (indent) {
+ attrs[ATTRIBUTE_INDENT] = String(indent);
+ }
+
+ if (id) {
+ attrs.id = id;
+ }
+
+ return ['p', attrs, 0];
+}
+
+export const toParagraphDOM = toDOM;
+export const getParagraphNodeAttrs = getAttrs;
+
+export function convertMarginLeftToIndentValue(marginLeft: string): number {
+ const ptValue = convertToCSSPTValue(marginLeft);
+ return clamp(
+ MIN_INDENT_LEVEL,
+ Math.floor(ptValue / INDENT_MARGIN_PT_SIZE),
+ MAX_INDENT_LEVEL
+ );
+}
+
+export default ParagraphNodeSpec; \ No newline at end of file
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index c38f84551..003ff6272 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -1,19 +1,30 @@
-import { Schema, NodeType } from "prosemirror-model";
-import {
- wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
- joinUp, joinDown, lift, selectParentNode
-} from "prosemirror-commands";
-import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list";
-import { undo, redo } from "prosemirror-history";
+import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
-import { Transaction, EditorState } from "prosemirror-state";
+import { Schema } from "prosemirror-model";
+import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, } from "prosemirror-schema-list";
+import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { TooltipTextMenu } from "./TooltipTextMenu";
-import { Statement } from "../northstar/model/idea/idea";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
export type KeyMap = { [key: string]: any };
+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) {
+ 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++;
+ 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: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ }
+ });
+ return tx2;
+};
export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
let keys: { [key: string]: any } = {}, type;
@@ -30,79 +41,149 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Shift-Mod-z", redo);
bind("Backspace", undoInputRule);
- if (!mac) {
- bind("Mod-y", redo);
- }
+ !mac && bind("Mod-y", redo);
bind("Alt-ArrowUp", joinUp);
bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift);
bind("Escape", selectParentNode);
- if (type = schema.marks.strong) {
- bind("Mod-b", toggleMark(type));
- bind("Mod-B", toggleMark(type));
- }
- if (type = schema.marks.em) {
- bind("Mod-i", toggleMark(type));
- bind("Mod-I", toggleMark(type));
- }
- if (type = schema.marks.underline) {
- bind("Mod-u", toggleMark(type));
- bind("Mod-U", toggleMark(type));
- }
- if (type = schema.marks.code) {
- bind("Mod-`", toggleMark(type));
- }
+ bind("Mod-b", toggleMark(schema.marks.strong));
+ bind("Mod-B", toggleMark(schema.marks.strong));
- if (type = schema.nodes.bullet_list) {
- bind("Ctrl-.", wrapInList(type));
- }
- if (type = schema.nodes.ordered_list) {
- bind("Ctrl-n", wrapInList(type));
- }
- if (type = schema.nodes.blockquote) {
- bind("Ctrl->", wrapIn(type));
+ bind("Mod-e", toggleMark(schema.marks.em));
+ bind("Mod-E", toggleMark(schema.marks.em));
+
+ bind("Mod-u", toggleMark(schema.marks.underline));
+ bind("Mod-U", toggleMark(schema.marks.underline));
+
+ bind("Mod-`", toggleMark(schema.marks.code));
+
+ bind("Ctrl-.", wrapInList(schema.nodes.bullet_list));
+
+ bind("Ctrl-n", wrapInList(schema.nodes.ordered_list));
+
+ bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+
+ // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // let tr = state.tr;
+ // tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+ // return true;
+ // }
+ // return false;
+ // });
+
+
+ let cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ return true;
+ }
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ mac && bind("Ctrl-Enter", cmd);
+
+
+ bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
+
+ bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
}
- if (type = schema.nodes.hard_break) {
- let br = type, cmd = chainCommands(exitCode, (state, dispatch) => {
- if (dispatch) {
- dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
- return true;
+
+ let hr = schema.nodes.horizontal_rule;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ 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);
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ let tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) { // couldn't sink into an existing list, so wrap in a new one
+ let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
+ let tx3 = updateBullets(tx2, schema);
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet promote fail");
}
- return false;
- });
- bind("Mod-Enter", cmd);
- bind("Shift-Enter", cmd);
- if (mac) {
- bind("Ctrl-Enter", cmd);
}
- }
- if (type = schema.nodes.list_item) {
- bind("Enter", splitListItem(type));
- bind("Shift-Tab", liftListItem(type));
- bind("Tab", sinkListItem(type));
- }
- if (type = schema.nodes.paragraph) {
- bind("Shift-Ctrl-0", setBlockType(type));
- }
- if (type = schema.nodes.code_block) {
- bind("Shift-Ctrl-\\", setBlockType(type));
- }
- if (type = schema.nodes.heading) {
- for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, setBlockType(type, { level: i }));
+ });
+
+ bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+
+ if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ let tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet demote fail");
}
- }
- if (type = schema.nodes.horizontal_rule) {
- let hr = type;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
- return true;
+ });
+
+ let splitMetadata = (marks: any, tx: Transaction) => {
+ marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ return tx;
+ };
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
+ if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
+ splitMetadata(marks, tx3);
+ if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
+ bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ dispatch(splitMetadata(marks, state.tr));
+ return false;
+ });
+ bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+ return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
});
- }
+ let path = (state.doc.resolve(state.selection.from - 1) as any).path;
+ let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+ let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
+ let text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ let whitespace = text.length - 1;
+ for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+ if (text.endsWith(":")) {
+ dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+ addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ }
+ return false;
+ });
- bind("Mod-s", TooltipTextMenu.insertStar);
return keys;
}
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 3b8396510..ebb9bda8a 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,14 +1,12 @@
-import {
- inputRules,
- wrappingInputRule,
- textblockTypeInputRule,
- smartQuotes,
- emDash,
- ellipsis
-} from "prosemirror-inputrules";
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model";
-
+import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
import { schema } from "./RichTextSchema";
+import { wrappingInputRule } from "./prosemirrorPatches";
+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: [
@@ -21,10 +19,29 @@ export const inpRules = {
// 1. ordered list
wrappingInputRule(
- /^(\d+)\.\s$/,
+ /^1\.\s$/,
+ schema.nodes.ordered_list,
+ () => {
+ return ({ mapStyle: "decimal", bulletStyle: 1 });
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
+ ),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^a\.\s$/,
schema.nodes.ordered_list,
- match => ({ order: +match[1] }),
- (match, node) => node.childCount + node.attrs.order === +match[1]
+ // match => {
+ () => {
+ return ({ mapStyle: "alpha", bulletStyle: 1 });
+ // return ({ order: +match[1] })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
),
// * bullet list
@@ -35,9 +52,144 @@ export const inpRules = {
// # heading
textblockTypeInputRule(
- new RegExp("^(#{1,6})\\s$"),
+ new RegExp(/^(#{1,6})\s$/),
schema.nodes.heading,
- match => ({ level: match[1].length })
- )
+ match => {
+ return ({ level: match[1].length });
+ }
+ ),
+
+ new InputRule(
+ new RegExp(/^#([0-9]+)\s$/),
+ (state, match, start, end) => {
+ let size = Number(match[1]);
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
+ return state.tr.deleteRange(start, end);
+ }
+ 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;
+ let sm = state.storedMarks || undefined;
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleAlign_" + heading] = "center";
+ 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: "center" })).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 ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ 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;
+ }
+ 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$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ 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;
+ }
+ 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$/),
+ (state, match, start, end) => {
+ let newNode = schema.nodes.footnote.create({});
+ let tr = state.tr;
+ tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+ return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+ }),
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // return true;
+ // }
]
};
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 9fdda4845..a5502577b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,6 +1,23 @@
-import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+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 { TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+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 { DocumentView } from "../views/nodes/DocumentView";
+import { DocumentManager } from "./DocumentManager";
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
+import { Transform } from "./Transform";
+import React = require("react");
+import { BoolCast, NumCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -13,23 +30,32 @@ export const nodes: { [index: string]: NodeSpec } = {
content: "block+"
},
- // :: NodeSpec A plain paragraph textblock. Represented in the DOM
- // as a `<p>` element.
- paragraph: {
+
+ footnote: {
+ group: "inline",
content: "inline*",
- group: "block",
- parseDOM: [{ tag: "p" }],
- toDOM() { return pDOM; }
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ parseDOM: [{ tag: "footnote" }]
},
- // starmine: {
- // inline: true,
- // attrs: { oldtext: { default: "" } },
- // group: "inline",
- // toDOM() { return ["star", "㊉"]; },
- // parseDOM: [{ tag: "star" }]
+ // // :: NodeSpec A plain paragraph textblock. Represented in the DOM
+ // // as a `<p>` element.
+ // paragraph: {
+ // content: "inline*",
+ // group: "block",
+ // parseDOM: [{ tag: "p" }],
+ // toDOM() { return pDOM; }
// },
+ paragraph: ParagraphNodeSpec,
+
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
content: "block+",
@@ -87,8 +113,6 @@ export const nodes: { [index: string]: NodeSpec } = {
visibility: { default: false },
text: { default: undefined },
textslice: { default: undefined },
- textlen: { default: 0 }
-
},
group: "inline",
toDOM(node) {
@@ -112,9 +136,12 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
- width: { default: "100px" },
+ width: { default: 100 },
alt: { default: null },
- title: { default: null }
+ title: { default: null },
+ float: { default: "left" },
+ location: { default: "onRight" },
+ docid: { default: "" }
},
group: "inline",
draggable: true,
@@ -128,13 +155,42 @@ export const nodes: { [index: string]: NodeSpec } = {
};
}
}],
- // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ // TODO if we don't define toDom, dragging the image crashes. Why?
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}` };
return ["img", { ...node.attrs, ...attrs }];
}
},
+ 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: {
@@ -173,35 +229,59 @@ export const nodes: { [index: string]: NodeSpec } = {
ordered_list: {
...orderedList,
content: 'list_item+',
- group: 'block'
+ group: 'block',
+ attrs: {
+ 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;
+ 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;
+ 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}` }];
+ }
},
- //this doesn't currently work for some reason
+
bullet_list: {
...bulletList,
content: 'list_item+',
group: 'block',
// parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
- // toDOM() { return ulDOM }
+ toDOM(node: Node<any>) {
+ return ['ul', 0];
+ }
},
- //bullet_list: {
- // content: 'list_item+',
- // group: 'block',
- //active: blockActive(schema.nodes.bullet_list),
- //enable: wrapInList(schema.nodes.bullet_list),
- //run: wrapInList(schema.nodes.bullet_list),
- //select: state => true,
- // },
list_item: {
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
...listItem,
- content: 'paragraph block*'
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ 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;
+ return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+ //return ["li", { class: `${map}` }, 0];
+ }
},
};
const emDOM: DOMOutputSpecArray = ["em", 0];
const strongDOM: DOMOutputSpecArray = ["strong", 0];
const codeDOM: DOMOutputSpecArray = ["code", 0];
-const underlineDOM: DOMOutputSpecArray = ["underline", 0];
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks: { [index: string]: MarkSpec } = {
@@ -212,7 +292,8 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
href: {},
location: { default: null },
- title: { default: null }
+ title: { default: null },
+ docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
},
inclusive: false,
parseDOM: [{
@@ -220,13 +301,17 @@ export const marks: { [index: string]: MarkSpec } = {
return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") };
}
}],
- toDOM(node: any) { return ["a", node.attrs, 0]; }
+ 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];
+ }
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }],
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
toDOM() { return emDOM; }
},
@@ -239,16 +324,6 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM() { return strongDOM; }
},
- underline: {
- parseDOM: [
- { tag: 'u' },
- { style: 'text-decoration=underline' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration:underline'
- }]
- },
-
strikethrough: {
parseDOM: [
{ tag: 'strike' },
@@ -278,24 +353,126 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
+ metadata: {
+ toDOM() {
+ return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+ }
+ },
+ metadataKey: {
+ toDOM() {
+ return ['span', { style: 'font-style:italic; ' }];
+ }
+ },
+ metadataVal: {
+ toDOM() {
+ return ['span'];
+ }
+ },
+
highlight: {
- parseDOM: [{ style: 'color: blue' }],
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ let style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ inclusive: true,
toDOM() {
return ['span', {
- style: 'color: blue'
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
}];
}
},
+ underline: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ let style = getComputedStyle(p);
+ if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ }
+ // { style: "text-decoration=underline" }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline;text-decoration-style:line'
+ }]
+ },
+
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"}`
}];
}
},
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ opened: { default: true },
+ 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);
+ return node.attrs.opened ?
+ ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
+ ['span', { class: "userMark-" + uid + " 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 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]];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
@@ -303,6 +480,24 @@ 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: {
@@ -371,7 +566,6 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
fontSize: { default: 10 }
},
- inclusive: false,
parseDOM: [{ style: 'font-size: 10px;' }],
toDOM: (node) => ['span', {
style: `font-size: ${node.attrs.fontSize}px;`
@@ -448,22 +642,21 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
};
-function getFontSize(element: any) {
- return parseFloat((getComputedStyle(element) as any).fontSize);
-}
export class ImageResizeView {
_handle: HTMLElement;
_img: HTMLElement;
_outer: HTMLElement;
- constructor(node: any, view: any, getPos: any) {
+ constructor(node: any, view: any, getPos: any, addDocTab: any) {
this._handle = document.createElement("span");
this._img = document.createElement("img");
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._img.setAttribute("src", node.attrs.src);
this._img.style.width = "100%";
@@ -476,32 +669,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 (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ 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}`;
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
};
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
document.removeEventListener("pointerup", onpointerup);
- view.dispatch(
- view.state.tr.setNodeMarkup(getPos(), null,
- { src: node.attrs.src, width: self._outer.style.width })
- .setSelection(view.state.selection));
+ 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;
}
@@ -518,72 +730,274 @@ 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
+ }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ this.node = node;
+ this.outerView = view;
+ this.getPos = getPos;
+
+ // The node's representation in the editor (empty, for now)
+ this.dom = document.createElement("footnote");
+ this.dom.addEventListener("pointerup", this.toggle, true);
+ // These are used when the footnote is selected
+ this.innerView = null;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ let tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ new Plugin({
+ view(newView) {
+ return FormattedTextBox.getToolTip(newView);
+ }
+ })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ this.innerView && this.innerView.destroy();
+ this.innerView = null;
+ this.dom.textContent = "";
+ }
+ dispatchInner(tr: any) {
+ let { state, transactions } = this.innerView.state.applyTransaction(tr);
+ this.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ 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);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.node)) return false;
+ this.node = node;
+ if (this.innerView) {
+ let state = this.innerView.state;
+ let start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ let overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+}
+
export class SummarizedView {
- // TODO: highlight text that is summarized. to find end of region, walk along mark
_collapsed: HTMLElement;
_view: any;
constructor(node: any, view: any, getPos: any) {
this._collapsed = document.createElement("span");
- this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉";
- this._collapsed.style.opacity = "0.5";
- this._collapsed.style.position = "relative";
- this._collapsed.style.width = "40px";
- this._collapsed.style.height = "20px";
- let self = this;
+ this._collapsed.className = this.className(node.attrs.visibility);
this._view = view;
const js = node.toJSON;
node.toJSON = function () {
-
return js.apply(this, arguments);
};
- this._collapsed.onpointerdown = function (e: any) {
- if (node.attrs.visibility) {
- // node.attrs.visibility = !node.attrs.visibility;
- let y = getPos();
- const attrs = { ...node.attrs };
- attrs.visibility = !attrs.visibility;
- let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight);
- let length = to - from;
- let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length);
- // update attrs of node
- attrs.text = newSelection.content();
- attrs.textslice = newSelection.content().toJSON();
- view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
- view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
- self._collapsed.textContent = "㊉";
- } else {
- // node.attrs.visibility = !node.attrs.visibility;
- let y = getPos();
- const attrs = { ...node.attrs };
- attrs.visibility = !attrs.visibility;
- view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
- let mark = view.state.schema.mark(view.state.schema.marks.highlight);
- view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1)));
- const from = view.state.selection.from;
- let size = node.attrs.text.size;
- view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark));
- self._collapsed.textContent = "㊀";
+
+ this._collapsed.onpointerdown = (e: any) => {
+ const visible = !node.attrs.visibility;
+ const attrs = { ...node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
}
+ view.dispatch(view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
+ setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
e.preventDefault();
e.stopPropagation();
+ this._collapsed.className = this.className(visible);
};
(this as any).dom = this._collapsed;
-
- }
- selectNode() {
}
+ selectNode() { }
- updateSummarizedText(start?: any, mark?: any) {
- let $start = this._view.state.doc.resolve(start);
+ deselectNode() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ updateSummarizedText(start?: any) {
+ let mark = this._view.state.schema.marks.highlight.create();
let endPos = start;
- let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight);
let visited = new Set();
for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
let skip = false;
this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
if (node.isLeaf && !visited.has(node) && !skip) {
- if (node.marks.includes(_mark)) {
+ if (node.marks.find((m: any) => m.type === mark.type)) {
visited.add(node);
endPos = i + node.nodeSize - 1;
}
@@ -591,10 +1005,7 @@ export class SummarizedView {
}
});
}
- return { from: start, to: endPos };
- }
-
- deselectNode() {
+ return TextSelection.create(this._view.state.doc, start, endPos);
}
}
// :: Schema
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 1d0916ac0..ff4451824 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -19,6 +19,7 @@ export interface ScriptSucccess {
export interface ScriptError {
success: false;
error: any;
+ result: any;
}
export type ScriptResult = ScriptSucccess | ScriptError;
@@ -27,7 +28,7 @@ export interface CompiledScript {
readonly compiled: true;
readonly originalScript: string;
readonly options: Readonly<ScriptOptions>;
- run(args?: { [name: string]: any }): ScriptResult;
+ run(args?: { [name: string]: any }, onError?: (res: any) => void, errorVal?: any): ScriptResult;
}
export interface CompileError {
@@ -100,7 +101,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
// let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
- let run = (args: { [name: string]: any } = {}): ScriptResult => {
+ let run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
let argsArray: any[] = [];
for (let name of customParams) {
if (name === "this") {
@@ -127,7 +128,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
if (batch) {
batch.end();
}
- return { success: false, error };
+ onError && onError(error);
+ return { success: false, error, result: errorVal };
}
};
return { compiled: true, run, originalScript, options };
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 9efef888d..df1b46b33 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -24,6 +24,9 @@ export namespace SelectionManager {
manager.SelectedDocuments.push(docView);
// console.log(manager.SelectedDocuments);
docView.props.whenActiveChanged(true);
+ } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
+ manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
+ manager.SelectedDocuments = [docView];
}
}
@action
@@ -38,7 +41,6 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
@@ -84,20 +86,4 @@ export namespace SelectionManager {
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments.slice();
}
- export function ViewsSortedHorizontally(): DocumentView[] {
- let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
- if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- return 0;
- });
- return sorted;
- }
- export function ViewsSortedVertically(): DocumentView[] {
- let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
- if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
- return 0;
- });
- return sorted;
- }
}
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..1541cd6b2
--- /dev/null
+++ b/src/client/util/SharingManager.tsx
@@ -0,0 +1,301 @@
+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 { CollectionVideoView } from "../views/collections/CollectionVideoView";
+import { CollectionPDFView } from "../views/collections/CollectionPDFView";
+import { CollectionView } from "../views/collections/CollectionView";
+
+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;
+ MainView.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;
+ MainView.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<CollectionVideoView | CollectionPDFView | 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/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index 55e0eb909..e6d6c471f 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -1,23 +1,9 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
-import React = require("react");
+import { FieldViewProps } from "../views/nodes/FieldView";
import "./TooltipTextMenu.scss";
+import React = require("react");
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
-import {
- faListUl,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
const SVG = "http://www.w3.org/2000/svg";
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 4672dd246..c82d3bc63 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -3,14 +3,13 @@ import { faListUl } from '@fortawesome/free-solid-svg-icons';
import { action, observable } from "mobx";
import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
-import { liftListItem, wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { wrapInList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
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,6 +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 { updateBullets } from './ProsemirrorExampleTransfer';
+import { DocumentDecorations } from '../views/DocumentDecorations';
const { toggleMark, setBlockType } = require("prosemirror-commands");
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
@@ -26,13 +28,13 @@ export class TooltipTextMenu {
public tooltip: HTMLElement;
private view: EditorView;
+ private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
- private listTypes: NodeType[];
- private editorProps: FieldViewProps & FormattedTextBoxProps;
+ private listTypes: (NodeType | any)[];
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
- private listTypeToIcon: Map<NodeType, string>;
+ private listTypeToIcon: Map<NodeType | any, string>;
//private link: HTMLAnchorElement;
private wrapper: HTMLDivElement;
private extras: HTMLDivElement;
@@ -57,15 +59,8 @@ export class TooltipTextMenu {
private _collapsed: boolean = false;
- @observable
- private _storedMarks: Mark<any>[] | null | undefined;
-
- public HackToFixTextSelectionGlitch: 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");
@@ -85,8 +80,6 @@ export class TooltipTextMenu {
this.dragElement(dragger);
- this._storedMarks = this.view.state.storedMarks;
-
// this.createCollapse();
// if (this._collapseBtn) {
// this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
@@ -126,10 +119,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"; }
@@ -165,13 +158,22 @@ export class TooltipTextMenu {
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, 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());
//list types
this.listTypeToIcon = new Map();
this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
- this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
+ 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());
@@ -185,12 +187,10 @@ 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) {
@@ -272,7 +272,7 @@ export class TooltipTextMenu {
if (DocumentManager.Instance.getDocumentView(f)) {
DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
}
- else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined);
+ else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight");
}
}));
}
@@ -290,6 +290,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
@@ -300,13 +301,17 @@ export class TooltipTextMenu {
{
handlers: {
dragComplete: action(() => {
- // let m = dragData.droppedDocuments;
- let linkDoc = dragData.linkDocument;
- let proto = Doc.GetProto(linkDoc);
- if (docView && docView.props.ContainingCollectionView) {
- proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
+ if (dragData.linkDocument) {
+ let linkDoc = dragData.linkDocument;
+ let proto = Doc.GetProto(linkDoc);
+ if (proto && docView) {
+ proto.sourceContext = docView.props.ContainingCollectionDoc;
+ }
+ let text = this.makeLink(linkDoc, 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
+ }
}
- linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
}),
},
hideSource: false
@@ -315,8 +320,6 @@ export class TooltipTextMenu {
e.preventDefault();
};
this.linkEditor.appendChild(this.linkDrag);
- // this.linkEditor.appendChild(this.linkText);
- // this.linkEditor.appendChild(linkBtn);
this.tooltip.appendChild(this.linkEditor);
}
@@ -390,17 +393,24 @@ export class TooltipTextMenu {
}
}
- makeLinkWithState = (state: EditorState, target: string, location: string) => {
- let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
- }
+ // makeLinkWithState = (state: EditorState, target: string, location: string) => {
+ // let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
+ // }
- makeLink = (target: string, location: string) => {
+ makeLink = (targetDoc: Doc, location: string): string => {
+ let target = Utils.prepend("/doc/" + targetDoc[Id]);
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 });
+ 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;
+ }
+ }
+ return "";
}
deleteLink = () => {
@@ -425,11 +435,13 @@ export class TooltipTextMenu {
}
public static insertStar(state: EditorState<any>, dispatch: any) {
- let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from });
- if (dispatch) {
- //console.log(newNode.attrs.text.toString());
- dispatch(state.tr.replaceSelectionWith(newNode));
- }
+ if (state.selection.empty) return false;
+ let mark = state.schema.marks.highlight.create();
+ let tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ let content = tr.selection.content();
+ 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;
}
@@ -489,25 +501,56 @@ export class TooltipTextMenu {
if (markType.name[0] === 'p') {
let size = this.fontSizeToNum.get(markType);
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;
+ }
+ }
}
else {
let fontName = this.fontStylesToName.get(markType);
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
- return toggleMark(markType)(view.state, view.dispatch, view);
- }
- else {
- return;
+ 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: 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);
}
}
//remove all node typeand apply the passed-in one to the selected text
- changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
- //remove old
- liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
- if (nodeType) { //add new
+ changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
+ //remove oldif (nodeType) { //add new
+ 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) => {
+ let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ view.dispatch(tx2);
+ })) {
+ let tx2 = view.state.tr;
+ let tx3 = nodeType ? updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle) : tx2;
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ view.dispatch(tx3);
+ }
}
}
@@ -536,7 +579,7 @@ export class TooltipTextMenu {
class: "summarize",
execEvent: "",
run: (state, dispatch) => {
- TooltipTextMenu.insertStar(state, dispatch);
+ TooltipTextMenu.insertStar(this.view.state, this.view.dispatch);
}
});
@@ -602,10 +645,9 @@ export class TooltipTextMenu {
if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
if (this._brushMarks && to - from > 0) {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
- this._brushMarks.forEach((mark: Mark) => {
+ Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
const markType = mark.type;
this.changeToMarkInGroup(markType, this.view, []);
-
});
}
}
@@ -799,9 +841,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.TextBar && DocumentDecorations.Instance.setTextBar(DocumentDecorations.Instance.TextBar);
+ 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;
@@ -849,8 +899,6 @@ export class TooltipTextMenu {
this.updateFontSizeDropdown("Various");
}
}
- !this.HackToFixTextSelectionGlitch &&
- this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); // bcz: what's the purpose of this line? It messes up text selection without the Hack.
this.update_mark_doms();
}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 156390fd3..7abb9d1ee 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -127,7 +127,7 @@ export namespace UndoManager {
export function StartBatch(batchName: string): Batch {
batchCounter++;
- if (batchCounter > 0) {
+ if (batchCounter > 0 && currentBatch === undefined) {
currentBatch = [];
}
return new Batch(batchName);
diff --git a/src/client/util/clamp.js b/src/client/util/clamp.js
new file mode 100644
index 000000000..9c7fd78a4
--- /dev/null
+++ b/src/client/util/clamp.js
@@ -0,0 +1,15 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = clamp;
+function clamp(min, val, max) {
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+} \ No newline at end of file
diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js
new file mode 100644
index 000000000..179557953
--- /dev/null
+++ b/src/client/util/convertToCSSPTValue.js
@@ -0,0 +1,43 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined;
+exports.default = convertToCSSPTValue;
+exports.toClosestFontPtSize = toClosestFontPtSize;
+
+// var _FontSizeCommandMenuButton = require('./ui/FontSizeCommandMenuButton');
+
+var SIZE_PATTERN = /([\d\.]+)(px|pt)/i;
+
+var PX_TO_PT_RATIO = exports.PX_TO_PT_RATIO = 0.7518796992481203; // 1 / 1.33.
+var PT_TO_PX_RATIO = exports.PT_TO_PX_RATIO = 1.33;
+
+function convertToCSSPTValue(styleValue) {
+ var matches = styleValue.match(SIZE_PATTERN);
+ if (!matches) {
+ return 0;
+ }
+ var value = parseFloat(matches[1]);
+ var unit = matches[2];
+ if (!value || !unit) {
+ return 0;
+ }
+ if (unit === 'px') {
+ value = PX_TO_PT_RATIO * value;
+ }
+ return value;
+}
+
+function toClosestFontPtSize(styleValue) {
+ var originalPTValue = convertToCSSPTValue(styleValue);
+
+ // if (_FontSizeCommandMenuButton.FONT_PT_SIZES.includes(originalPTValue)) {
+ // return originalPTValue;
+ // }
+
+ return _FontSizeCommandMenuButton.FONT_PT_SIZES.reduce(function (prev, curr) {
+ return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev;
+ }, Number.NEGATIVE_INFINITY);
+} \ No newline at end of file
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
new file mode 100644
index 000000000..269423482
--- /dev/null
+++ b/src/client/util/prosemirrorPatches.js
@@ -0,0 +1,139 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var prosemirrorInputRules = require('prosemirror-inputrules');
+var prosemirrorTransform = require('prosemirror-transform');
+var prosemirrorModel = require('prosemirror-model');
+
+exports.liftListItem = liftListItem;
+exports.sinkListItem = sinkListItem;
+exports.wrappingInputRule = wrappingInputRule;
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to lift the list item around the selection up into
+// a wrapping list.
+function liftListItem(itemType) {
+ return function (tx, dispatch) {
+ var ref = tx.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ if (!dispatch) { return true }
+ if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+ { return liftToOuterList(tx, dispatch, itemType, range) }
+ else // Outer list node
+ { return liftOutOfList(tx, dispatch, range) }
+ }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+ var end = range.end, endOfList = range.$to.end(range.depth);
+ if (end < endOfList) {
+ // There are siblings after the lifted items, which must become
+ // children of the last item
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+ new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+ range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+ }
+ dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+ return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+ var list = range.parent;
+ // Merge the list items into a single big item
+ for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+ pos -= list.child(i).nodeSize;
+ tr.delete(pos - 1, pos + 1);
+ }
+ var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+ var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+ var parent = $start.node(-1), indexBefore = $start.index(-1);
+ if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+ item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+ var start = $start.pos, end = start + item.nodeSize;
+ // Strip off the surrounding list. At the sides where we're not at
+ // the end of the list, the existing list is closed. At sides where
+ // this is the end, it is overwritten to its end.
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+ new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+ .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+ atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
+ dispatch(tr.scrollIntoView());
+ return true
+}
+
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to sink the list item around the selection down
+// into an inner list.
+function sinkListItem(itemType) {
+ return function (state, dispatch) {
+ var ref = state.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ var startIndex = range.startIndex;
+ if (startIndex == 0) { return false }
+ var parent = range.parent, nodeBefore = parent.child(startIndex - 1);
+ if (nodeBefore.type != itemType) { return false; }
+
+ if (dispatch) {
+ var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
+ var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
+ let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create({ ...parent.attrs, fontSize: parent.attrs.fontSize ? parent.attrs.fontSize - 4 : undefined }, inner)))),
+ nestedBefore ? 3 : 1, 0);
+ var before = range.start, after = range.end;
+ dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
+ before, after, slice, 1, true))
+ .scrollIntoView());
+ }
+ return true
+ }
+}
+
+function findWrappingOutside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var around = parent.contentMatchAt(startIndex).findWrapping(type);
+ if (!around) { return null }
+ var outer = around.length ? around[0] : type;
+ return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
+}
+
+function findWrappingInside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var inner = parent.child(startIndex);
+ var inside = type.contentMatch.findWrapping(inner.type);
+ if (!inside) { return null }
+ var lastType = inside.length ? inside[inside.length - 1] : type;
+ var innerMatch = lastType.contentMatch;
+ for (var i = startIndex; innerMatch && i < endIndex; i++) { innerMatch = innerMatch.matchType(parent.child(i).type); }
+ if (!innerMatch || !innerMatch.validEnd) { return null }
+ return inside
+}
+function findWrapping(range, nodeType, attrs, innerRange, customWithAttrs = null) {
+ if (innerRange === void 0) innerRange = range;
+ let withAttrs = (type) => ({ type: type, attrs: null });
+ var around = findWrappingOutside(range, nodeType);
+ var inner = around && findWrappingInside(innerRange, nodeType);
+ if (!inner) { return null }
+ return around.map(withAttrs).concat({ type: nodeType, attrs: attrs }).concat(inner.map(customWithAttrs ? customWithAttrs : withAttrs))
+}
+function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWithAttrs = null) {
+ return new prosemirrorInputRules.InputRule(regexp, function (state, match, start, end) {
+ var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
+ var tr = state.tr.delete(start, end);
+ var $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs, undefined, customWithAttrs);
+ if (!wrapping) { return null }
+ tr.wrap(range, wrapping);
+ var before = tr.doc.resolve(start - 1).nodeBefore;
+ if (before && before.type == nodeType && prosemirrorTransform.canJoin(tr.doc, start - 1) &&
+ (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); }
+ return tr
+ })
+} \ No newline at end of file
diff --git a/src/client/util/toCSSLineSpacing.js b/src/client/util/toCSSLineSpacing.js
new file mode 100644
index 000000000..939d11a0e
--- /dev/null
+++ b/src/client/util/toCSSLineSpacing.js
@@ -0,0 +1,64 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = toCSSLineSpacing;
+
+
+// Line spacing names and their values.
+var LINE_SPACING_100 = exports.LINE_SPACING_100 = '125%';
+var LINE_SPACING_115 = exports.LINE_SPACING_115 = '138%';
+var LINE_SPACING_150 = exports.LINE_SPACING_150 = '165%';
+var LINE_SPACING_200 = exports.LINE_SPACING_200 = '232%';
+
+var SINGLE_LINE_SPACING = exports.SINGLE_LINE_SPACING = LINE_SPACING_100;
+var DOUBLE_LINE_SPACING = exports.DOUBLE_LINE_SPACING = LINE_SPACING_200;
+
+var NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/;
+
+// Normalize the css line-height vlaue to percentage-based value if applicable.
+// Also, it calibrates the incorrect line spacing value exported from Google
+// Doc.
+function toCSSLineSpacing(source) {
+ if (!source) {
+ return '';
+ }
+
+ var strValue = String(source);
+
+ // e.g. line-height: 1.5;
+ if (NUMBER_VALUE_PATTERN.test(strValue)) {
+ var numValue = parseFloat(strValue);
+ strValue = String(Math.round(numValue * 100)) + '%';
+ }
+
+ // Google Doc exports line spacing with wrong values. For instance:
+ // - Single => 100%
+ // - 1.15 => 115%
+ // - Double => 200%
+ // But the actual CSS value measured in Google Doc is like this:
+ // - Single => 125%
+ // - 1.15 => 138%
+ // - Double => 232%
+ // The following `if` block will calibrate the value if applicable.
+
+ if (strValue === '100%') {
+ return LINE_SPACING_100;
+ }
+
+ if (strValue === '115%') {
+ return LINE_SPACING_115;
+ }
+
+ if (strValue === '150%') {
+ return LINE_SPACING_150;
+ }
+
+ if (strValue === '200%') {
+ return LINE_SPACING_200;
+ }
+
+ // e.g. line-height: 15px;
+ return strValue;
+} \ No newline at end of file
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index e2c0de8af..8f112de0c 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -124,7 +124,9 @@
}
.icon-background {
- pointer-events: none;
+ pointer-events: all;
+ height:100%;
+ margin-top: 15px;
background-color: transparent;
width: 35px;
text-align: center;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 760736501..5d452e72e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -246,10 +246,11 @@ export class ContextMenu extends React.Component {
this.selectedIndex--;
}
e.preventDefault();
- } else if (e.key === "Enter") {
+ } else if (e.key === "Enter" || e.key === "Tab") {
const item = this.flatItems[this.selectedIndex];
- item.event();
+ item && item.event({ x: this.pageX, y: this.pageY });
this.closeMenu();
+ e.preventDefault();
}
}
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 90f7be33f..330b94afa 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -10,7 +10,7 @@ library.add(faAngleRight);
export interface OriginalMenuProps {
description: string;
- event: () => void;
+ event: (stuff?: any) => void;
undoable?: boolean;
icon: IconProp; //maybe should be optional (icon?)
closeMenu?: () => void;
@@ -44,7 +44,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
if (this.props.undoable !== false) {
batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
}
- await this.props.event();
+ await this.props.event({ x: e.clientX, y: e.clientY });
batch && batch.end();
}
}
@@ -89,17 +89,17 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
);
} else if ("subitems" in this.props) {
let submenu = !this.overItem ? (null) :
- <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
+ <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px" }}>
{this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
</div>;
return (
- <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}>
+ <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}>
{this.props.icon ? (
- <span className="icon-background">
+ <span className="icon-background" onMouseEnter={this.onPointerLeave}>
<FontAwesomeIcon icon={this.props.icon} size="sm" />
</span>
) : null}
- <div className="contextMenu-description">
+ <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} >
{this.props.description}
<FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} />
</div>
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
new file mode 100644
index 000000000..8cd419bbe
--- /dev/null
+++ b/src/client/views/DocumentButtonBar.scss
@@ -0,0 +1,129 @@
+@import "globalCssVariables";
+
+$linkGap : 3px;
+
+.linkFlyout {
+ grid-column: 2/4;
+}
+
+.linkButton-empty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.linkButton-nonempty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.documentButtonBar {
+ margin-top: $linkGap;
+ grid-column: 1/4;
+ width: max-content;
+ height: auto;
+ display: flex;
+ flex-direction: row;
+}
+
+.linkButtonWrapper {
+ pointer-events: auto;
+ padding-right: 5px;
+ width: 25px;
+}
+
+.linkButton-linker {
+ height: 20px;
+ width: 20px;
+ text-align: center;
+ border-radius: 50%;
+ pointer-events: auto;
+ color: $dark-color;
+ border: $dark-color 1px solid;
+}
+
+.linkButton-linker:hover {
+ cursor: pointer;
+ transform: scale(1.05);
+}
+
+.linkButton-empty,
+.linkButton-nonempty {
+ height: 20px;
+ width: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ pointer-events: auto;
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
+}
+
+.templating-menu {
+ position: absolute;
+ pointer-events: auto;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.templating-button,
+.docDecs-tagButton {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ font-size: 14;
+ background-color: $dark-color;
+ color: $light-color;
+ text-align: center;
+ cursor: pointer;
+
+ &:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ }
+}
+
+#template-list {
+ position: absolute;
+ top: 25px;
+ left: 0px;
+ width: max-content;
+ font-family: $sans-serif;
+ font-size: 12px;
+ background-color: $light-color-secondary;
+ padding: 2px 12px;
+ list-style: none;
+
+ .templateToggle, .chromeToggle {
+ text-align: left;
+ }
+
+ input {
+ margin-right: 10px;
+ }
+}
+
+@-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
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
new file mode 100644
index 000000000..9e2d41621
--- /dev/null
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -0,0 +1,367 @@
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, observable, runInAction } from "mobx";
+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";
+import { LinkManager } from '../util/LinkManager';
+import { UndoManager } from "../util/UndoManager";
+import './DocumentButtonBar.scss';
+import './collections/ParentDocumentSelector.scss';
+import { LinkMenu } from "./linking/LinkMenu";
+import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
+import { TemplateMenu } from "./TemplateMenu";
+import { Template, Templates } from "./Templates";
+import React = require("react");
+import { DocumentView } from './nodes/DocumentView';
+import { ParentDocSelector } from './collections/ParentDocumentSelector';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+library.add(faLink);
+library.add(faTag);
+library.add(faTimes);
+library.add(faArrowAltCircleDown);
+library.add(faArrowAltCircleUp);
+library.add(faStopCircle);
+library.add(faCheckCircle);
+library.add(faCloudUploadAlt);
+library.add(faSyncAlt);
+library.add(faShare);
+
+const cloud: IconProp = "cloud-upload-alt";
+const fetch: IconProp = "sync-alt";
+
+@observer
+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 _tooltipoff = React.createRef<HTMLDivElement>();
+ private _textDoc?: Doc;
+ private _linkDrag?: UndoManager.Batch;
+ public static Instance: DocumentButtonBar;
+
+ constructor(props: { views: DocumentView[] }) {
+ super(props);
+ DocumentButtonBar.Instance = this;
+ }
+
+ @observable public pushIcon: IconProp = "arrow-alt-circle-up";
+ @observable public pullIcon: IconProp = "arrow-alt-circle-down";
+ @observable public pullColor: string = "white";
+ @observable public isAnimatingFetch = false;
+ @observable public openHover = false;
+ public pullColorAnimating = false;
+
+ private pullAnimating = false;
+ private pushAnimating = false;
+
+ public startPullOutcome = action((success: boolean) => {
+ if (!this.pullAnimating) {
+ this.pullAnimating = true;
+ this.pullIcon = success ? "check-circle" : "stop-circle";
+ setTimeout(() => runInAction(() => {
+ this.pullIcon = "arrow-alt-circle-down";
+ this.pullAnimating = false;
+ }), 1000);
+ }
+ });
+
+ public startPushOutcome = action((success: boolean) => {
+ if (!this.pushAnimating) {
+ this.pushAnimating = true;
+ this.pushIcon = success ? "check-circle" : "stop-circle";
+ setTimeout(() => runInAction(() => {
+ this.pushIcon = "arrow-alt-circle-up";
+ this.pushAnimating = false;
+ }), 1000);
+ }
+ });
+
+ public setPullState = action((unchanged: boolean) => {
+ this.isAnimatingFetch = false;
+ if (!this.pullColorAnimating) {
+ this.pullColorAnimating = true;
+ this.pullColor = unchanged ? "lawngreen" : "red";
+ setTimeout(this.clearPullColor, 1000);
+ }
+ });
+
+ private clearPullColor = action(() => {
+ this.pullColor = "white";
+ this.pullColorAnimating = false;
+ });
+
+ onLinkerButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.addEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ document.addEventListener("pointerup", this.onLinkerButtonUp);
+ }
+
+ onEmbedButtonDown = (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);
+ }
+
+ onLinkerButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ e.stopPropagation();
+ }
+
+ onEmbedButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ e.stopPropagation();
+ }
+
+ @action
+ onLinkerButtonMoved = (e: PointerEvent): void => {
+ if (this._linkerButton.current !== null) {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ 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] : []);
+ this._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;
+ }
+ },
+ },
+ hideSource: false
+ });
+ }
+ e.stopPropagation();
+ }
+
+ @action
+ onEmbedButtonMoved = (e: PointerEvent): void => {
+ if (this._embedButton.current !== null) {
+ document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+ document.removeEventListener("pointerup", this.onEmbedButtonUp);
+
+ let dragDocView = this.props.views[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();
+ }
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.addEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ document.addEventListener("pointerup", this.onLinkButtonUp);
+ }
+
+ onLinkButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ e.stopPropagation();
+ }
+
+ onLinkButtonMoved = async (e: PointerEvent) => {
+ if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+ DragLinksAsDocuments(this._linkButton.current, e.x, e.y, this.props.views[0].props.Document);
+ }
+ 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>
+ </div>
+ );
+ }
+
+ private get targetDoc() {
+ return this.props.views[0].props.Document;
+ }
+
+ considerGoogleDocsPush = () => {
+ let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+ if (!canPush) return (null);
+ let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
+ let icon: IconProp = published ? (this.pushIcon as any) : cloud;
+ return (
+ <div className={"linkButtonWrapper"}>
+ <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+ DocumentButtonBar.hasPushedHack = false;
+ this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
+ </div>
+ </div>
+ );
+ }
+
+ considerGoogleDocsPull = () => {
+ let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+ let dataDoc = Doc.GetProto(this.targetDoc);
+ if (!canPull || !dataDoc[GoogleRef]) return (null);
+ let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
+ icon = this.openHover ? "share" : icon;
+ let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
+ let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
+ return (
+ <div className={"linkButtonWrapper"}>
+ <div
+ title={title}
+ className="linkButton-linker"
+ style={{
+ backgroundColor: this.pullColor,
+ transition: "0.2s ease all"
+ }}
+ onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
+ onPointerLeave={() => runInAction(() => this.openHover = false)}
+ onClick={e => {
+ if (e.altKey) {
+ e.preventDefault();
+ window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
+ } else {
+ this.clearPullColor();
+ DocumentButtonBar.hasPulledHack = false;
+ this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
+ dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+ }
+ }}>
+ <FontAwesomeIcon
+ style={{
+ WebkitAnimation: animation,
+ MozAnimation: animation
+ }}
+ className="documentdecorations-icon"
+ icon={icon}
+ size="sm"
+ />
+ </div>
+ </div>
+ );
+ }
+
+ public static hasPushedHack = false;
+ public static hasPulledHack = false;
+
+ considerTooltip = () => {
+ let thisDoc = this.props.views[0].props.Document;
+ let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
+ if (!isTextDoc) return null;
+ this._textDoc = thisDoc;
+ return (
+ <div className="tooltipwrapper">
+ <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+ {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
+ </div>
+ </div>
+
+ );
+ }
+
+ onTooltipOff = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (this._textDoc) {
+ if (this._tooltipoff.current) {
+ if (this._tooltipoff.current.title === "Hide Tooltip") {
+ this._tooltipoff.current.title = "Show Tooltip";
+ this._textDoc.tooltip = "hi";
+ }
+ else {
+ this._tooltipoff.current.title = "Hide Tooltip";
+ }
+ }
+ }
+ }
+
+ get metadataMenu() {
+ return (
+ <div className="linkButtonWrapper">
+ <Flyout anchorPoint={anchorPoints.TOP_LEFT}
+ content={<MetadataEntryMenu docs={() => this.props.views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
+ <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
+ </Flyout>
+ </div>
+ );
+ }
+
+ render() {
+ let linkButton = null;
+ if (this.props.views.length > 0) {
+ let selFirst = this.props.views[0];
+
+ let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
+ linkButton = (<Flyout
+ anchorPoint={anchorPoints.RIGHT_TOP}
+ content={<LinkMenu docView={selFirst}
+ addDocTab={selFirst.props.addDocTab}
+ changeFlyout={emptyFunction} />}>
+ <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
+ </Flyout >);
+ }
+
+ let templates: Map<Template, boolean> = new Map();
+ Array.from(Object.values(Templates.TemplateList)).map(template =>
+ templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
+
+ return (<div className="documentButtonBar">
+ <div className="linkButtonWrapper">
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
+ </div>
+ <div className="linkButtonWrapper">
+ <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
+ </div>
+ </div>
+ <div className="linkButtonWrapper">
+ <TemplateMenu docs={this.props.views} templates={templates} />
+ </div>
+ {this.metadataMenu}
+ {this.considerEmbed()}
+ {this.considerGoogleDocsPush()}
+ {this.considerGoogleDocsPull()}
+ <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
+ where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight");
+ return true;
+ }} />
+ {/* {this.considerTooltip()} */}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ac8497bd0..32346165d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -154,7 +154,7 @@ $linkGap : 3px;
.link-button-container {
margin-top: $linkGap;
grid-column: 1/4;
- width: auto;
+ width: max-content;
height: auto;
display: flex;
flex-direction: row;
@@ -257,7 +257,7 @@ $linkGap : 3px;
padding: 2px 12px;
list-style: none;
- .templateToggle {
+ .templateToggle, .chromeToggle {
text-align: left;
}
@@ -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 a28088032..4f9bdbe9c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,35 +1,30 @@
-import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons';
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
+import { ObjectField } from '../../new_fields/ObjectField';
import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
-import { URLField } from '../../new_fields/URLField';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { emptyFunction, Utils } from "../../Utils";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
import { DocumentManager } from "../util/DocumentManager";
-import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
+import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
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 { DocumentView, PositionDocument } from "./nodes/DocumentView";
+import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
+import { DocumentView, swapViews } from "./nodes/DocumentView";
import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
-import { LinkMenu } from "./nodes/LinkMenu";
-import { TemplateMenu } from "./TemplateMenu";
-import { Template, Templates } from "./Templates";
import React = require("react");
-import { RichTextField } from '../../new_fields/RichTextField';
-import { LinkManager } from '../util/LinkManager';
-import { MetadataEntryMenu } from './MetadataEntryMenu';
-import { ImageBox } from './nodes/ImageBox';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -45,28 +40,20 @@ library.add(faCloudUploadAlt);
library.add(faSyncAlt);
library.add(faShare);
-const cloud: IconProp = "cloud-upload-alt";
-const fetch: IconProp = "sync-alt";
-
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
static Instance: DocumentDecorations;
private _isPointerDown = false;
private _resizing = "";
- private keyinput: React.RefObject<HTMLInputElement>;
+ private _keyinput: React.RefObject<HTMLInputElement>;
private _resizeBorderWidth = 16;
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
- private _linkButton = React.createRef<HTMLDivElement>();
- private _linkerButton = React.createRef<HTMLDivElement>();
- private _embedButton = React.createRef<HTMLDivElement>();
- private _tooltipoff = React.createRef<HTMLDivElement>();
- private _textDoc?: Doc;
private _downX = 0;
private _downY = 0;
private _iconDoc?: Doc = undefined;
private _resizeUndo?: UndoManager.Batch;
- private _linkDrag?: UndoManager.Batch;
+ private _radiusDown = [0, 0];
@observable private _minimizedX = 0;
@observable private _minimizedY = 0;
@observable private _title: string = "";
@@ -76,58 +63,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable private _opacity = 1;
@observable private _removeIcon = false;
@observable public Interacting = false;
- @observable private _isMoving = false;
@observable public pushIcon: IconProp = "arrow-alt-circle-up";
@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;
- public pullColorAnimating = false;
-
- private pullAnimating = false;
- private pushAnimating = false;
-
- public startPullOutcome = action((success: boolean) => {
- if (!this.pullAnimating) {
- this.pullAnimating = true;
- this.pullIcon = success ? "check-circle" : "stop-circle";
- setTimeout(() => runInAction(() => {
- this.pullIcon = "arrow-alt-circle-down";
- this.pullAnimating = false;
- }), 1000);
- }
- });
-
- public startPushOutcome = action((success: boolean) => {
- if (!this.pushAnimating) {
- this.pushAnimating = true;
- this.pushIcon = success ? "check-circle" : "stop-circle";
- setTimeout(() => runInAction(() => {
- this.pushIcon = "arrow-alt-circle-up";
- this.pushAnimating = false;
- }), 1000);
- }
- });
-
- public setPullState = action((unchanged: boolean) => {
- this.isAnimatingFetch = false;
- if (!this.pullColorAnimating) {
- this.pullColorAnimating = true;
- this.pullColor = unchanged ? "lawngreen" : "red";
- setTimeout(this.clearPullColor, 1000);
- }
- });
-
- private clearPullColor = action(() => {
- this.pullColor = "white";
- this.pullColorAnimating = false;
- });
constructor(props: Readonly<{}>) {
super(props);
DocumentDecorations.Instance = this;
- this.keyinput = React.createRef();
+ this._keyinput = React.createRef();
reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false);
}
@@ -141,17 +88,31 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (text[0] === '#') {
this._fieldKey = text.slice(1, text.length);
this._title = this.selectionTitle;
+ } else if (text.startsWith("::")) {
+ let targetID = text.slice(2, text.length);
+ let promoteDoc = SelectionManager.SelectedDocuments()[0];
+ DocUtils.Publish(promoteDoc.props.Document, targetID, promoteDoc.props.addDocument, promoteDoc.props.removeDocument);
} else if (text.startsWith(">")) {
let fieldTemplateView = SelectionManager.SelectedDocuments()[0];
SelectionManager.DeselectAll();
let fieldTemplate = fieldTemplateView.props.Document;
- let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document;
- let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
- let proto = Doc.GetProto(docTemplate);
- Doc.MakeTemplate(fieldTemplate, metaKey, proto);
- if (text.startsWith(">>")) {
- proto.detailedLayout = proto.layout;
- proto.miniLayout = ImageBox.LayoutString(metaKey);
+ let containerView = fieldTemplateView.props.ContainingCollectionView;
+ let docTemplate = fieldTemplateView.props.ContainingCollectionDoc;
+ if (containerView && docTemplate) {
+ let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
+ if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) {
+ const fd = fieldTemplate.data;
+ fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd));
+ }
+ fieldTemplate.title = metaKey;
+ Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
+ if (text.startsWith(">>")) {
+ 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}"}`);
+ }
}
}
else {
@@ -200,14 +161,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.onBackgroundUp(e);
}
+ @observable _forceUpdate = 0;
+ _lastBox = { x: 0, y: 0, r: 0, b: 0 };
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
- return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
+ let x = this._forceUpdate;
+ this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
if (documentView.props.renderDepth === 0 ||
Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
return bounds;
}
let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
+ if (transform.TranslateX === 0 && transform.TranslateY === 0) {
+ setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
+ return this._lastBox;
+ }
+
var [sptX, sptY] = transform.transformPoint(0, 0);
let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
return {
@@ -215,6 +184,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ return this._lastBox;
}
onBackgroundDown = (e: React.PointerEvent): void => {
@@ -228,11 +198,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@action
onBackgroundMove = (e: PointerEvent): void => {
let dragDocView = SelectionManager.SelectedDocuments()[0];
+ let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
- const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
- let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document));
- dragData.xOffset = xoff;
- dragData.yOffset = yoff;
+ dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
this.Interacting = true;
this._hidden = true;
@@ -274,7 +242,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onCloseUp = async (e: PointerEvent) => {
e.stopPropagation();
if (e.button === 0) {
- const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+ const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc;
SelectionManager.SelectedDocuments().map(dv => {
recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
@@ -335,22 +303,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
}
- if (!this._removeIcon) {
- if (selectedDocs.length === 1) {
- this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].toggleMinimized());
- } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
- let docViews = SelectionManager.ViewsSortedVertically();
- let topDocView = docViews[0];
- let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
- if (ind !== -1) {
- topDocView.templates.splice(ind, 1);
- topDocView.props.Document.subBulletDocs = undefined;
- } else {
- topDocView.addTemplate(Templates.Bullet);
- topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ if (!this._removeIcon && selectedDocs.length === 1) { // if you click on the top-left button when just 1 doc is selected, then collapse it. not sure why we don't do it for multiple selections
+ this.getIconDoc(selectedDocs[0]).then(async icon => {
+ let minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) {
+ let scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint(
+ NumCast(minimizedDoc.x) - NumCast(selectedDocs[0].Document.x), NumCast(minimizedDoc.y) - NumCast(selectedDocs[0].Document.y));
+ SelectionManager.DeselectAll();
+ DocumentManager.Instance.animateBetweenPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
}
- }
+ });
}
this._removeIcon = false;
}
@@ -362,10 +324,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let doc = selected[0].props.Document;
let iconDoc = Docs.Create.IconDocument(layoutString);
iconDoc.isButton = true;
- iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
- iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
+
+ IconBox.AutomaticTitle(iconDoc);
//iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
- iconDoc.proto!.isMinimized = false;
iconDoc.width = Number(MINIMIZED_ICON_SIZE);
iconDoc.height = Number(MINIMIZED_ICON_SIZE);
iconDoc.x = NumCast(doc.x);
@@ -388,14 +349,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
moveIconDoc(iconDoc: Doc) {
let selView = SelectionManager.SelectedDocuments()[0];
- let zoom = NumCast(selView.props.Document.zoomBasis, 1);
- let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom).
+ let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).
transformPoint(this._minimizedX - 12, this._minimizedY - 12);
iconDoc.x = where[0] + NumCast(selView.props.Document.x);
iconDoc.y = where[1] + NumCast(selView.props.Document.y);
}
- _radiusDown = [0, 0];
@action
onRadiusDown = (e: React.PointerEvent): void => {
e.stopPropagation();
@@ -408,16 +367,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.addEventListener("pointermove", this.onRadiusMove);
document.addEventListener("pointerup", this.onRadiusUp);
}
- if (!this._isMoving) {
- 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)).
- map(d => d.borderRounding = "0%");
- }
}
onRadiusMove = (e: PointerEvent): void => {
- this._isMoving = true;
let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
- 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)).
+ dist = dist < 3 ? 0 : dist;
+ let usingRule = false;
+ SelectionManager.SelectedDocuments().map(dv => {
+ let ruleProvider = dv.props.ruleProvider;
+ let heading = NumCast(dv.props.Document.heading);
+ 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)).
map(d => d.borderRounding = `${Math.min(100, dist)}%`);
e.stopPropagation();
e.preventDefault();
@@ -428,15 +390,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.preventDefault();
this._isPointerDown = false;
this._resizeUndo && this._resizeUndo.end();
- this._isMoving = false;
document.removeEventListener("pointermove", this.onRadiusMove);
document.removeEventListener("pointerup", this.onRadiusUp);
}
+ _lastX = 0;
+ _lastY = 0;
@action
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
if (e.button === 0) {
+ this._lastX = e.clientX;
+ this._lastY = e.clientY;
this._isPointerDown = true;
this._resizing = e.currentTarget.id;
this.Interacting = true;
@@ -448,100 +413,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
}
- onLinkerButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onLinkerButtonMoved);
- document.addEventListener("pointermove", this.onLinkerButtonMoved);
- document.removeEventListener("pointerup", this.onLinkerButtonUp);
- document.addEventListener("pointerup", this.onLinkerButtonUp);
- }
-
- 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);
- }
-
- onLinkerButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onLinkerButtonMoved);
- document.removeEventListener("pointerup", this.onLinkerButtonUp);
- e.stopPropagation();
- }
-
- onEmbedButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- e.stopPropagation();
- }
-
- @action
- onLinkerButtonMoved = (e: PointerEvent): void => {
- if (this._linkerButton.current !== null) {
- document.removeEventListener("pointermove", this.onLinkerButtonMoved);
- document.removeEventListener("pointerup", this.onLinkerButtonUp);
- let selDoc = SelectionManager.SelectedDocuments()[0];
- let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
- let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
- FormattedTextBox.InputBoxOverlay = undefined;
- this._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;
- }
- },
- },
- hideSource: false
- });
- }
- 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();
- }
-
- onLinkButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.addEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- document.addEventListener("pointerup", this.onLinkButtonUp);
- }
-
- onLinkButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- e.stopPropagation();
- }
-
- onLinkButtonMoved = async (e: PointerEvent) => {
- if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);
- }
- e.stopPropagation();
- }
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
@@ -552,46 +423,50 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let dX = 0, dY = 0, dW = 0, dH = 0;
+ let moveX = e.clientX - this._lastX; // e.movementX;
+ let moveY = e.clientY - this._lastY; // e.movementY;
+ this._lastX = e.clientX;
+ this._lastY = e.clientY;
+
switch (this._resizing) {
case "":
break;
case "documentDecorations-topLeftResizer":
dX = -1;
dY = -1;
- dW = -(e.movementX);
- dH = -(e.movementY);
+ dW = -moveX;
+ dH = -moveY;
break;
case "documentDecorations-topRightResizer":
- dW = e.movementX;
+ dW = moveX;
dY = -1;
- dH = -(e.movementY);
+ dH = -moveY;
break;
case "documentDecorations-topResizer":
dY = -1;
- dH = -(e.movementY);
+ dH = -moveY;
break;
case "documentDecorations-bottomLeftResizer":
dX = -1;
- dW = -(e.movementX);
- dH = e.movementY;
+ dW = -moveX;
+ dH = moveY;
break;
case "documentDecorations-bottomRightResizer":
- dW = e.movementX;
- dH = e.movementY;
+ dW = moveX;
+ dH = moveY;
break;
case "documentDecorations-bottomResizer":
- dH = e.movementY;
+ dH = moveY;
break;
case "documentDecorations-leftResizer":
dX = -1;
- dW = -(e.movementX);
+ dW = -moveX;
break;
case "documentDecorations-rightResizer":
- dW = e.movementX;
+ dW = moveX;
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);
@@ -605,7 +480,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
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 || (!BoolCast(doc.ignoreAspect) && nwidth && nheight);
+ 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;
+ }
if (fixedAspect && (!nwidth || !nheight)) {
proto.nativeWidth = nwidth = doc.width || 0;
proto.nativeHeight = nheight = doc.height || 0;
@@ -616,7 +496,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
}
doc.width = actualdW;
- if (fixedAspect) doc.height = nheight / nwidth * doc.width;
+ if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
else doc.height = actualdH;
}
else {
@@ -624,7 +504,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
}
doc.height = actualdH;
- if (fixedAspect) doc.width = nwidth / nheight * doc.height;
+ if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
else doc.width = actualdW;
}
} else {
@@ -666,138 +546,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return "-unset-";
}
- changeFlyoutContent = (): void => {
-
- }
- // buttonOnPointerUp = (e: React.PointerEvent): void => {
- // e.stopPropagation();
- // }
-
- considerEmbed = () => {
- let thisDoc = SelectionManager.SelectedDocuments()[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>
- </div>
- );
- }
-
- private get targetDoc() {
- return SelectionManager.SelectedDocuments()[0].props.Document;
- }
-
- considerGoogleDocsPush = () => {
- let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
- if (!canPush) return (null);
- let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
- if (!published) {
- this.targetDoc.autoHeight = true;
+ TextBar: HTMLDivElement | undefined;
+ setTextBar = (ele: HTMLDivElement) => {
+ if (ele) {
+ this.TextBar = ele;
+ TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.appendChild(TooltipTextMenu.Toolbar);
}
- let icon: IconProp = published ? (this.pushIcon as any) : cloud;
- return (
- <div className={"linkButtonWrapper"}>
- <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
- DocumentDecorations.hasPushedHack = false;
- this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
- }}>
- <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
- </div>
- </div>
- );
}
-
- considerGoogleDocsPull = () => {
- let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
- let dataDoc = Doc.GetProto(this.targetDoc);
- if (!canPull || !dataDoc[GoogleRef]) return (null);
- let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
- icon = this.openHover ? "share" : icon;
- let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
- let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
- return (
- <div className={"linkButtonWrapper"}>
- <div
- title={title}
- className="linkButton-linker"
- style={{
- backgroundColor: this.pullColor,
- transition: "0.2s ease all"
- }}
- onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
- onPointerLeave={() => runInAction(() => this.openHover = false)}
- onClick={e => {
- if (e.altKey) {
- e.preventDefault();
- window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
- } else {
- this.clearPullColor();
- DocumentDecorations.hasPulledHack = false;
- this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
- dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
- }
- }}>
- <FontAwesomeIcon
- style={{
- WebkitAnimation: animation,
- MozAnimation: animation
- }}
- className="documentdecorations-icon"
- icon={icon}
- size="sm"
- />
- </div>
- </div>
- );
- }
-
- public static hasPushedHack = false;
- public static hasPulledHack = false;
-
- considerTooltip = () => {
- let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
- let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
- if (!isTextDoc) return null;
- this._textDoc = thisDoc;
- return (
- <div className="tooltipwrapper">
- <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
- {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
- </div>
- </div>
-
- );
- }
-
- onTooltipOff = (e: React.PointerEvent): void => {
- e.stopPropagation();
- if (this._textDoc) {
- if (this._tooltipoff.current) {
- if (this._tooltipoff.current.title === "Hide Tooltip") {
- this._tooltipoff.current.title = "Show Tooltip";
- this._textDoc.tooltip = "hi";
- }
- else {
- this._tooltipoff.current.title = "Hide Tooltip";
- }
- }
- }
- }
-
- get metadataMenu() {
- return (
- <div className="linkButtonWrapper">
- <Flyout anchorPoint={anchorPoints.TOP_LEFT}
- content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
- <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
- </Flyout>
- </div>
- );
- }
-
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -809,46 +564,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
</div>);
- let linkButton = null;
- if (SelectionManager.SelectedDocuments().length > 0) {
- let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
- linkButton = (<Flyout
- anchorPoint={anchorPoints.RIGHT_TOP}
- content={<LinkMenu docView={selFirst}
- addDocTab={selFirst.props.addDocTab}
- changeFlyout={this.changeFlyoutContent} />}>
- <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
- </Flyout >);
- }
-
- let templates: Map<Template, boolean> = new Map();
- Array.from(Object.values(Templates.TemplateList)).map(template => {
- let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => {
- // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1;
- // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- // return 0;
- // });
- let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
- let temps = doc.props.Document.templates;
- if (temps instanceof List) {
- temps.map(temp => {
- if (temp !== Templates.Bullet.Layout || i === 0) {
- res.push(temp);
- }
- });
- }
- return res;
- }, [] as string[]);
- let checked = false;
- docTemps.forEach(temp => {
- if (template.Layout === temp) {
- checked = true;
- }
- });
- templates.set(template, checked);
- });
-
bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
const borderRadiusDraggerWidth = 15;
@@ -870,9 +585,9 @@ 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) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px",
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
opacity: this._opacity
@@ -880,7 +595,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{minimizeIcon}
{this._edtingTitle ?
- <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
+ <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
<div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
<div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>
<FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
@@ -896,22 +611,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
<div className="link-button-container">
- <div className="linkButtonWrapper">
- <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
- </div>
- <div className="linkButtonWrapper">
- <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
- </div>
- </div>
- <div className="linkButtonWrapper">
- <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
- </div>
- {this.metadataMenu}
- {this.considerEmbed()}
- {this.considerGoogleDocsPush()}
- {this.considerGoogleDocsPull()}
- {/* {this.considerTooltip()} */}
+ <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
</div>
</div >
</div>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 58f5d662d..8e86f58ee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -29,7 +29,8 @@ export interface EditableProps {
contents: any;
fontStyle?: string;
fontSize?: number;
- height?: number;
+ height?: number | "auto";
+ maxHeight?: number;
display?: string;
autosuggestProps?: {
resetValue: () => void;
@@ -151,7 +152,7 @@ export class EditableView extends React.Component<EditableProps> {
if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
return (
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
- style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
+ style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }}
onClick={this.onClick}>
<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span>
</div>
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d0464bd5f..c519991a5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,6 +6,7 @@ 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";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -30,7 +31,7 @@ export default class KeyManager {
}
public handle = async (e: KeyboardEvent) => {
- let keyname = e.key.toLowerCase();
+ let keyname = e.key && e.key.toLowerCase();
this.handleGreedy(keyname);
if (modifiers.includes(keyname)) {
@@ -72,6 +73,7 @@ export default class KeyManager {
main.toggleColorPicker(true);
SelectionManager.DeselectAll();
DictationManager.Controls.stop();
+ SharingManager.Instance.close();
break;
case "delete":
case "backspace":
@@ -88,9 +90,6 @@ export default class KeyManager {
});
}, "delete");
break;
- case "enter":
- SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document));
- break;
}
return {
@@ -144,7 +143,7 @@ export default class KeyManager {
return { stopPropagation: false, preventDefault: false };
}
}
- MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined);
+ MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform, undefined);
break;
case "arrowleft":
if (document.activeElement) {
@@ -152,7 +151,7 @@ export default class KeyManager {
return { stopPropagation: false, preventDefault: false };
}
}
- MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform);
+ MainView.Instance.mainFreeform && CollectionDockingView.CloseRightSplit(MainView.Instance.mainFreeform);
break;
case "backspace":
if (document.activeElement) {
@@ -166,7 +165,7 @@ export default class KeyManager {
break;
case "o":
let target = SelectionManager.SelectedDocuments()[0];
- target && target.fullScreenClicked();
+ target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target);
break;
case "r":
preventDefault = false;
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 5437b26d6..9cc220a1d 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,7 +1,8 @@
@import "globalCssVariables";
.inkingCanvas {
- opacity: 0.99;
+ // opacity: 0.99;
+ touch-action: none;
.jsx-parser {
position: absolute;
@@ -34,7 +35,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..9ab320eab 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -87,7 +87,7 @@ 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)
});
this.inkData = data;
}
@@ -150,9 +150,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}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 3f40642b5..ee8b77050 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -9,9 +9,10 @@ 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 } from "../../new_fields/Types";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
+import { StrCast, NumCast, Cast } from "../../new_fields/Types";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { Utils } from "../../Utils";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -44,12 +45,41 @@ export class InkingControl extends React.Component {
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;
+ // 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 oldColor = StrCast(targetDoc.backgroundColor);
- targetDoc.backgroundColor = this._selectedColor;
+ let matchedColor = this._selectedColor;
+ const cvd = view.props.ContainingCollectionDoc;
+ let ruleProvider = view.props.ruleProvider;
+ if (cvd) {
+ if (!cvd.colorPalette) {
+ let defaultPalette = ["rg(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(cvd.colorPalette, listSpec("string"));
+ if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette);
+ }
+ let cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
+ let closest = 0;
+ let dist = 10000000;
+ let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
+ for (let i = 0; i < cp.length; i++) {
+ let cpcol = Utils.fromRGBAstr(cp[i]);
+ let d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g));
+ if (d < dist) {
+ dist = d;
+ closest = i;
+ }
+ }
+ cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")";
+ cvd.colorPalette = new List(cp);
+ matchedColor = cp[closest];
+ 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);
+
return {
target: targetDoc,
previous: oldColor
@@ -62,7 +92,6 @@ export class InkingControl extends React.Component {
});
}
});
-
@action
switchWidth = (width: string): void => {
this._selectedWidth = width;
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bc0975c86..4cd860da2 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -238,6 +238,13 @@ ul#add-options-list {
flex-direction: column;
}
+.expandFlyoutButton {
+ position: absolute;
+ top: 30px;
+ right: 30px;
+ cursor: pointer;
+}
+
.mainView-libraryHandle {
width: 20px;
height: 40px;
@@ -268,44 +275,4 @@ ul#add-options-list {
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 0e687737d..3bd898ac0 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -7,6 +7,9 @@ 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";
+
+AssignAllExtensions();
let swapDocs = async () => {
let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
@@ -32,13 +35,16 @@ let swapDocs = 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";
- CurrentUserUtils.UserDocument.chromeStatus = "disabled";
- await swapDocs();
+ if (info.id !== "__guest__") {
+ // a guest will not have an id registered
+ 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";
+ CurrentUserUtils.UserDocument.chromeStatus = "disabled";
+ await swapDocs();
+ }
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 0839e1114..000000000
--- a/src/client/views/MainOverlayTextBox.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { action, observable, reaction, trace } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
-import { Doc } 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 _textAutoHeight: 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, false) || 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._textAutoHeight = autoHeight;
- 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 && div.parentElement.style.bottom ? 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], [FormattedTextBox.InputBoxOverlay.props.DataDoc]);
- const [left, top] = this._textXf().inverse().transformPoint(0, 0);
- dragData.xOffset = e.clientX - left;
- dragData.yOffset = 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: Doc | undefined, location: string) => {
- if (true) { // location === "onRight") { need to figure out stack to add "inTab"
- CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
- }
- }
- 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 textRect = this._textTargetDiv.getBoundingClientRect();
- let s = this._textXf().Scale;
- let location = this._textBottom ? textRect.bottom : textRect.top;
- let hgt = this._textAutoHeight || 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}
- ChromeHeight={this.ChromeHeight}
- isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
- 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.tsx b/src/client/views/MainView.tsx
index f28844009..ba4224875 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { 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 { 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, faChevronRight, faEllipsisV } 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';
@@ -7,24 +7,25 @@ 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, HeightSym, 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 } 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, returnOne, returnTrue, Utils } from '../../Utils';
import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
+import { Docs, DocumentOptions } from '../documents/Documents';
import { ClientUtils } from '../util/ClientUtils';
import { DictationManager } from '../util/DictationManager';
import { SetupDrag } from '../util/DragManager';
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 { UndoManager } from '../util/UndoManager';
+import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionTreeView } from './collections/CollectionTreeView';
import { ContextMenu } from './ContextMenu';
@@ -32,14 +33,13 @@ import { DocumentDecorations } from './DocumentDecorations';
import KeyManager from './GlobalKeyHandler';
import { InkingControl } from './InkingControl';
import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
+import MainViewModal from './MainViewModal';
import { DocumentView } from './nodes/DocumentView';
-import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
-import PresModeMenu from './presentationview/PresentationModeMenu';
-import { PresBox } from './nodes/PresBox';
+import { OverlayView } from './OverlayView';
+import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
@observer
export class MainView extends React.Component {
@@ -53,6 +53,8 @@ export class MainView extends React.Component {
@observable private dictationDisplayState = false;
@observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
+ public hasActiveModal = false;
+
public overlayTimeout: NodeJS.Timeout | undefined;
public initiateDictationFade = () => {
@@ -60,10 +62,17 @@ export class MainView extends React.Component {
this.overlayTimeout = setTimeout(() => {
this.dictationOverlayVisible = false;
this.dictationSuccess = undefined;
+ this.hasActiveModal = false;
setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
}, duration);
}
+ private urlState: HistoryUtil.DocUrl;
+
+ @computed private get userDoc() {
+ return CurrentUserUtils.UserDocument;
+ }
+
public cancelDictationFade = () => {
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
@@ -72,7 +81,7 @@ export class MainView extends React.Component {
}
@computed private get mainContainer(): Opt<Doc> {
- return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
+ return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace;
}
@computed get mainFreeform(): Opt<Doc> {
let docs = DocListCast(this.mainContainer!.data);
@@ -81,7 +90,10 @@ export class MainView extends React.Component {
public isPointerDown = false;
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
- CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ if (!("presentationView" in doc)) {
+ doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
+ }
+ this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
}
}
@@ -126,21 +138,23 @@ export class MainView extends React.Component {
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 });
+ if (this.userDoc) {
+ reaction(() => {
+ let workspaces = this.userDoc.workspaces;
+ let recent = this.userDoc.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 + this.userDoc[HeightSym]() * 0.00001;
+ return libraryHeight;
+ }, (libraryHeight: number) => {
+ if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) {
+ this.userDoc.height = libraryHeight;
+ }
+ (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true;
+ }, { fireImmediately: true });
+ }
}
componentWillUnMount() {
@@ -153,6 +167,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) {
@@ -161,6 +176,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)));
+ }
}
}
}
@@ -192,6 +212,8 @@ 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();
}
@@ -217,74 +239,113 @@ 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);
- if (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(
+ () => {
+ let docking = CollectionDockingView.Instance;
+ return docking && docking.initialized;
+ },
+ initialized => {
+ if (initialized && received) {
+ DocServer.GetRefField(received).then(field => {
+ if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
+ CollectionDockingView.AddRightSplit(field, undefined);
+ }
+ });
+ }
+ },
+ );
+ }
+ let doc: Opt<Doc>;
+ if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, 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;
+ let freeformOptions: DocumentOptions = {
+ x: 0,
+ y: 400,
+ width: this.pwidth * .7,
+ height: this.pheight,
+ title: "My Blank Collection"
+ };
+ 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([this.userDoc, freeformDoc], JSON.stringify(dockingLayout), {}, id);
+ if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) {
+ const list = Cast((workspaces).data, listSpec(Doc));
+ if (list) {
+ if (!this.userDoc.linkManagerDoc) {
+ let linkManagerDoc = new Doc();
+ linkManagerDoc.allLinks = new List<Doc>([]);
+ this.userDoc.linkManagerDoc = linkManagerDoc;
+ }
+ list.push(mainDoc);
+ mainDoc.title = `Workspace ${list.length}`;
}
- 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);
}
+ // 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);
}
@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) {
+ 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();
+ }
+ 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();
}
- 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);
+ let col: Opt<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) {
+ if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) {
const l = Cast(col.data, listSpec(Doc));
if (l) {
runInAction(() => CollectionTreeView.NotifsCol = col);
}
}
}, 100);
+ return true;
}
onDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -306,19 +367,22 @@ export class MainView extends React.Component {
}
@observable flyoutWidth: number = 250;
+ @observable flyoutTranslate: boolean = true;
@computed get dockingContent() {
let flyoutWidth = this.flyoutWidth;
+ let countWidth = this.flyoutTranslate;
let mainCont = this.mainContainer;
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}>
+ <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${countWidth ? flyoutWidth : 0}px`, transform: `translate(${countWidth ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
{!mainCont ? (null) :
<DocumentView Document={mainCont}
DataDoc={undefined}
addDocument={undefined}
- addDocTab={emptyFunction}
+ addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
onClick={undefined}
+ ruleProvider={undefined}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
@@ -326,12 +390,12 @@ export class MainView extends React.Component {
PanelHeight={this.getPHeight}
renderDepth={0}
backgroundColor={returnEmptyString}
- selectOnLoad={false}
focus={emptyFunction}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
zoomToScale={emptyFunction}
getScale={returnOne}
/>}
@@ -350,6 +414,23 @@ 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);
@@ -364,57 +445,94 @@ export class MainView extends React.Component {
document.removeEventListener("pointerup", this.onPointerUp);
}
flyoutWidthFunc = () => this.flyoutWidth;
- addDocTabFunc = (doc: Doc) => {
+ addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string) => {
+ if (where === "close") {
+ return CollectionDockingView.CloseRightSplit(doc);
+ }
if (doc.dockingConfig) {
this.openWorkspace(doc);
+ return true;
} else {
- CollectionDockingView.Instance.AddRightSplit(doc, undefined);
+ return CollectionDockingView.AddRightSplit(doc, undefined);
}
}
@computed
get flyout() {
- let sidebar = CurrentUserUtils.UserDocument.sidebar;
- if (!(sidebar instanceof Doc)) return (null);
- let sidebarDoc = sidebar;
+ let sidebar: FieldResult<Field>;
+ if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) {
+ return (null);
+ }
return <DocumentView
- Document={sidebarDoc}
+ Document={sidebar}
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}
- selectOnLoad={false}
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>
- <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>;
+ if (!this.userDoc) {
+ return (<div>{this.dockingContent}</div>);
+ }
+ let sidebar = this.userDoc.sidebar;
+ if (!(sidebar instanceof Doc)) {
+ return (null);
+ }
+ return (
+ <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
+ <div 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>);
+ }
+
+ @computed get expandButton() {
+ return !this.flyoutTranslate ? (<div className="expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => {
+ runInAction(() => {
+ this.flyoutWidth = 250;
+ this.flyoutTranslate = true;
+ });
+ }}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
}
selected = (tool: InkTool) => {
@@ -433,10 +551,25 @@ export class MainView extends React.Component {
}
}
+ setWriteMode = (mode: DocServer.WriteMode) => {
+ console.log(DocServer.WriteMode[mode]);
+ const mode1 = mode;
+ const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
+ DocServer.setFieldWriteMode("x", mode1);
+ DocServer.setFieldWriteMode("y", mode1);
+ DocServer.setFieldWriteMode("width", mode1);
+ DocServer.setFieldWriteMode("height", mode1);
+
+ DocServer.setFieldWriteMode("panX", mode2);
+ DocServer.setFieldWriteMode("panY", mode2);
+ DocServer.setFieldWriteMode("scale", mode2);
+ DocServer.setFieldWriteMode("viewType", mode2);
+ }
+
+
@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" }));
@@ -449,18 +582,21 @@ export class MainView extends React.Component {
// 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][] = [
+ // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
+
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<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>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
[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 }} >
+ return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 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>
@@ -476,6 +612,7 @@ export class MainView extends React.Component {
<FontAwesomeIcon icon={btn[1]} size="sm" />
</button>
</div></li>)}
+ <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>
<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} />
@@ -486,6 +623,7 @@ export class MainView extends React.Component {
<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))}>{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}</button></li>
</ul>
</div>
</div >;
@@ -501,12 +639,8 @@ export class MainView extends React.Component {
/* @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() {
- let logoutRef = React.createRef<HTMLDivElement>();
-
return [
this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
- <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -517,52 +651,41 @@ export class MainView extends React.Component {
this.isSearchVisible = !this.isSearchVisible;
}
- private get dictationOverlay() {
- let display = this.dictationOverlayVisible;
+ @computed private get dictationOverlay() {
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 (
- <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>
+ <MainViewModal
+ contents={result}
+ isDisplayed={this.dictationOverlayVisible}
+ interactive={false}
+ dialogueBoxStyle={dialogueBoxStyle}
+ overlayStyle={overlayStyle}
+ />
);
}
- @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); });
- return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
- }
-
render() {
return (
<div id="main-div">
{this.dictationOverlay}
+ <SharingManager />
+ <GoogleAuthenticationManager />
<DocumentDecorations />
{this.mainContent}
- {this.miniPresentation}
<PreviewCursor />
<ContextMenu />
{this.nodesMenu()}
{this.miscButtons}
<PDFMenu />
- <MainOverlayTextBox firstinstance={true} />
<OverlayView />
</div >
);
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..b7fdd6168
--- /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 (
+ <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/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.scss b/src/client/views/OverlayView.scss
index dc122497f..26c2e0e1e 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -39,4 +39,9 @@
width: 10px;
height: 10px;
cursor: nwse-resize;
+}
+
+.overlayView-doc {
+ z-index: 1;
+ position: absolute;
} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 2f2579057..95c7b2683 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,9 +1,17 @@
import * as React from "react";
import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Utils } from "../../Utils";
+import { observable, action, trace, computed } from "mobx";
+import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse } from "../../Utils";
import './OverlayView.scss';
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { DocListCast, Doc } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
+import { DocumentView } from "./nodes/DocumentView";
+import { Transform } from "../util/Transform";
+import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { NumCast } from "../../new_fields/Types";
export type OverlayDisposer = () => void;
@@ -132,10 +140,73 @@ export class OverlayView extends React.Component {
return remove;
}
+ @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) {
+ d.x = e.clientX + offsetx;
+ d.y = e.clientY + offsety;
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ let onPointerUp = action((e: PointerEvent) => {
+ document.removeEventListener("pointermove", onPointerMove);
+ document.removeEventListener("pointerup", onPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ let onPointerDown = (e: React.PointerEvent) => {
+ offsetx = NumCast(d.x) - e.clientX;
+ offsety = NumCast(d.y) - e.clientY;
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", onPointerMove);
+ document.addEventListener("pointerup", onPointerUp);
+ };
+ return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}>
+ <DocumentView
+ Document={d}
+ ChromeHeight={returnZero}
+ // isSelected={returnFalse}
+ // select={emptyFunction}
+ // layoutKey={"layout"}
+ bringToFront={emptyFunction}
+ ruleProvider={undefined}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={returnOne}
+ PanelWidth={returnOne}
+ PanelHeight={returnOne}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={1}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne} />
+ </div>;
+ });
+ }
+
render() {
return (
- <div>
- {this._elements}
+ <div className="overlayView" id="overlayView">
+ <div>
+ {this._elements}
+ </div>
+ {this.overlayDocs}
</div>
);
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index d8e161ab6..1aed51e64 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -51,7 +51,7 @@ export class PreviewCursor extends React.Component<{}> {
// tests for URL and makes web document
let re: any = /^https?:\/\//g;
if (re.test(e.clipboardData.getData("text/plain"))) {
- const url = e.clipboardData.getData("text/plain")
+ const url = e.clipboardData.getData("text/plain");
PreviewCursor._addDocument(Docs.Create.WebDocument(url, {
title: url, width: 300, height: 300,
// nativeWidth: 300, nativeHeight: 472.5,
@@ -93,9 +93,7 @@ export class PreviewCursor extends React.Component<{}> {
@action
onKeyPress = (e: KeyboardEvent) => {
- // Mixing events between React and Native is finicky. In FormattedTextBox, we set the
- // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
- // the keyPress here. 112-
+ // Mixing events between React and Native is finicky.
//if not these keys, make a textbox if preview cursor is active!
if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" &&
e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" &&
@@ -103,8 +101,8 @@ export class PreviewCursor extends React.Component<{}> {
e.key !== "NumLock" &&
(e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys
!e.key.startsWith("Arrow") &&
- !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
- if (!e.ctrlKey && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
+ !e.defaultPrevented) {
+ if ((!e.ctrlKey || (e.keyCode >= 48 && e.keyCode <= 57)) && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
PreviewCursor.Visible = false;
}
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 2b862a81e..8ef9f3be6 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -10,12 +10,15 @@ import { emptyFunction } from "../../Utils";
import { ScriptCast } from "../../new_fields/Types";
import { CompileScript } from "../util/Scripting";
import { ScriptField } from "../../new_fields/ScriptField";
+import { DragManager } from "../util/DragManager";
+import { EditableView } from "./EditableView";
export interface ScriptBoxProps {
onSave: (text: string, onError: (error: string) => void) => void;
onCancel?: () => void;
initialText?: string;
showDocumentIcons?: boolean;
+ setParams?: (p: string[]) => void;
}
@observer
@@ -56,23 +59,49 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
onFocus = this.onFocus;
onBlur = this.onBlur;
}
+ let params = <EditableView
+ contents={""}
+ display={"block"}
+ maxHeight={72}
+ height={35}
+ fontSize={28}
+ GetValue={() => ""}
+ SetValue={(value: string) => this.props.setParams && this.props.setParams(value.split(" ").filter(s => s !== " ")) ? true : true}
+ />;
return (
<div className="scriptBox-outerDiv">
+ <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
+ <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea>
+ <div style={{ background: "beige" }} >{params}</div>
+ </div>
<div className="scriptBox-toolbar">
<button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button>
<button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button>
</div>
- <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea>
</div>
);
}
- public static EditClickScript(doc: Doc, fieldKey: string) {
+ //let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
+ public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, prewrapper?: string, postwrapper?: string) {
let overlayDisposer: () => void = emptyFunction;
const script = ScriptCast(doc[fieldKey]);
let originalText: string | undefined = undefined;
- if (script) originalText = script.script.originalScript;
+ if (script) {
+ originalText = script.script.originalScript;
+ if (prewrapper && originalText.startsWith(prewrapper)) {
+ originalText = originalText.substr(prewrapper.length);
+ }
+ if (postwrapper && originalText.endsWith(postwrapper)) {
+ originalText = originalText.substr(0, originalText.length - postwrapper.length);
+ }
+ }
// tslint:disable-next-line: no-unnecessary-callback-wrapper
- let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+ let params: string[] = [];
+ let setParams = (p: string[]) => params.splice(0, params.length, ...p);
+ let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
+ if (prewrapper) {
+ text = prewrapper + text + (postwrapper ? postwrapper : "");
+ }
const script = CompileScript(text, {
params: { this: Doc.name },
typecheck: false,
@@ -83,9 +112,12 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
onError(script.errors.map(error => error.messageText).join("\n"));
return;
}
+
+ params.length && DragManager.StartButtonDrag([], text, "a script", {}, params, (button: Doc) => { }, clientX, clientY);
+
doc[fieldKey] = new ScriptField(script);
overlayDisposer();
}} showDocumentIcons />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${doc.title || ""} OnClick` });
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: title });
}
}
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/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index e05195ca0..1eb380e0b 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -135,19 +135,17 @@ export class ScriptingRepl extends React.Component {
this.commands.push({ command: this.commandString, result: script.errors });
return;
}
- const result = script.run({ args: this.args });
- if (!result.success) {
- this.commands.push({ command: this.commandString, result: result.error.toString() });
- return;
- }
- this.commands.push({ command: this.commandString, result: result.result });
- this.commandsHistory.push(this.commandString);
+ const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() }));
+ if (result.success) {
+ this.commands.push({ command: this.commandString, result: result.result });
+ this.commandsHistory.push(this.commandString);
- this.maybeScrollToBottom();
+ this.maybeScrollToBottom();
- this.commandString = "";
- this.commandBuffer = "";
- this.historyIndex = -1;
+ this.commandString = "";
+ this.commandBuffer = "";
+ this.historyIndex = -1;
+ }
break;
}
case "ArrowUp": {
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
deleted file mode 100644
index 33cb63df5..000000000
--- a/src/client/views/SearchBox.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import * as rp from 'request-promise';
-import { Doc } from '../../new_fields/Doc';
-import { Id } from '../../new_fields/FieldSymbols';
-import { NumCast } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
-import { SetupDrag } from '../util/DragManager';
-import { SearchItem } from './search/SearchItem';
-import "./SearchBox.scss";
-import { Utils } from '../../Utils';
-
-library.add(faSearch);
-library.add(faObjectGroup);
-
-@observer
-export class SearchBox extends React.Component {
- @observable
- searchString: string = "";
-
- @observable private _open: boolean = false;
- @observable private _resultsOpen: boolean = false;
-
- @observable
- private _results: Doc[] = [];
-
- @action.bound
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- this.searchString = e.target.value;
- }
-
- @action
- submitSearch = async () => {
- let query = this.searchString;
- //gets json result into a list of documents that can be used
- const results = await this.getResults(query);
-
- runInAction(() => {
- this._resultsOpen = true;
- this._results = results;
- });
- }
-
- @action
- getResults = async (query: string) => {
- let response = await rp.get(Utils.prepend('/search'), {
- qs: {
- query
- }
- });
- let res: string[] = JSON.parse(response);
- const fields = await DocServer.GetRefFields(res);
- const docs: Doc[] = [];
- for (const id of res) {
- const field = fields[id];
- if (field instanceof Doc) {
- docs.push(field);
- }
- }
- return docs;
- }
-
- @action
- handleClickFilter = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (className !== "filter-button" && className !== "filter-form") {
- this._open = false;
- }
-
- }
-
- @action
- handleClickResults = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (id !== "result") {
- this._resultsOpen = false;
- this._results = [];
- }
-
- }
-
- componentWillMount() {
- document.addEventListener('mousedown', this.handleClickFilter, false);
- document.addEventListener('mousedown', this.handleClickResults, false);
- }
-
- componentWillUnmount() {
- document.removeEventListener('mousedown', this.handleClickFilter, false);
- document.removeEventListener('mousedown', this.handleClickResults, false);
- }
-
- @action
- toggleFilterDisplay = () => {
- this._open = !this._open;
- }
-
- enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- this.submitSearch();
- }
- }
-
- collectionRef = React.createRef<HTMLSpanElement>();
- startDragCollection = async () => {
- const results = await this.getResults(this.searchString);
- const docs = results.map(doc => {
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- });
- let x = 0;
- let y = 0;
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- const size = 200;
- const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
- if (aspect > 1) {
- doc.height = size;
- doc.width = size / aspect;
- } else if (aspect > 0) {
- doc.width = size;
- doc.height = size * aspect;
- } else {
- doc.width = size;
- doc.height = size;
- }
- doc.zoomBasis = 1;
- x += 250;
- if (x > 1000) {
- x = 0;
- y += 300;
- }
- }
- return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
- }
-
- // Useful queries:
- // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
- // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
- render() {
- return (
- <div>
- <div className="searchBox-container">
- <div className="searchBox-bar">
- <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
- <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
- </span>
- <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
- className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
- style={{ width: this._resultsOpen ? "500px" : undefined }} />
- {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
- {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
- </div>
- {this._resultsOpen ? (
- <div className="searchBox-results">
- {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)}
- </div>
- ) : null}
- </div>
- {this._open ? (
- <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
- <div className="filter-form" id="header">Filter Search Results</div>
- <div className="filter-form" id="option">
- filter by collection, key, type of node
- </div>
-
- </div>
- ) : null}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index fd4b2420d..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-import { SetupDrag } from "../util/DragManager";
-
-
-export interface SearchProps {
- doc: Doc;
-}
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-export class SearchItem extends React.Component<SearchProps> {
-
- onClick = () => {
- DocumentManager.Instance.jumpToDocument(this.props.doc, false);
- }
-
- //needs help
- // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
-
-
- public static DocumentIcon(layout: string) {
- let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
- onPointerEnter = (e: React.PointerEvent) => {
- Doc.BrushDoc(this.props.doc);
- }
- onPointerLeave = (e: React.PointerEvent) => {
- Doc.UnBrushDoc(this.props.doc);
- }
-
- collectionRef = React.createRef<HTMLDivElement>();
- startDocDrag = () => {
- let doc = this.props.doc;
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- }
- render() {
- return (
- <div className="search-item" ref={this.collectionRef} id="result"
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
- <div className="search-title" id="result" >title: {this.props.doc.title}</div>
- {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
- {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 393e97a7e..9e5e62e03 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,16 +1,14 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../new_fields/Doc";
-import { List } from "../../new_fields/List";
-import './DocumentDecorations.scss';
-import { DocumentView } from "./nodes/DocumentView";
-import { Template } from "./Templates";
-import React = require("react");
-import { undoBatch } from "../util/UndoManager";
import { DocumentManager } from "../util/DocumentManager";
-import { NumCast } from "../../new_fields/Types";
import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
+import { undoBatch } from "../util/UndoManager";
+import './DocumentDecorations.scss';
+import { DocumentView } from "./nodes/DocumentView";
+import { Template, Templates } from "./Templates";
+import React = require("react");
+import { Doc } from "../../new_fields/Doc";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -30,6 +28,17 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
}
}
}
+@observer
+class OtherToggle extends React.Component<{ checked: boolean, name: string, toggle: (event: React.ChangeEvent<HTMLInputElement>) => void }> {
+ render() {
+ return (
+ <li className="chromeToggle">
+ <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event)} />
+ {this.props.name}
+ </li>
+ );
+ }
+}
export interface TemplateMenuProps {
docs: DocumentView[];
@@ -41,23 +50,23 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@observable private _hidden: boolean = true;
dragRef = React.createRef<HTMLUListElement>();
- constructor(props: TemplateMenuProps) {
- super(props);
+ toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => {
+ this.props.docs.map(dv => dv.setCustomView(e.target.checked));
}
- toggleFloat = (e: React.MouseEvent): void => {
+ toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
SelectionManager.DeselectAll();
let topDocView = this.props.docs[0];
let topDoc = topDocView.props.Document;
let xf = topDocView.props.ScreenToLocalTransform();
- let ex = e.clientX;
- let ey = e.clientY;
+ let ex = e.target.clientLeft;
+ let ey = e.target.clientTop;
undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))();
- if (!topDoc.z) {
+ if (e.target.checked) {
setTimeout(() => {
let newDocView = DocumentManager.Instance.getDocumentView(topDoc);
if (newDocView) {
- let de = new DragManager.DocumentDragData([topDoc], [undefined]);
+ let de = new DragManager.DocumentDragData([topDoc]);
de.moveDocument = topDocView.props.moveDocument;
let xf = newDocView.ContentDiv!.getBoundingClientRect();
DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, {
@@ -80,55 +89,53 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- if (template.Name === "Bullet") {
- let topDocView = this.props.docs[0];
- topDocView.addTemplate(template);
- topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
- } else {
- this.props.docs.map(d => d.addTemplate(template));
- }
- this.props.templates.set(template, true);
+ this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase());
} else {
- if (template.Name === "Bullet") {
- let topDocView = this.props.docs[0];
- topDocView.removeTemplate(template);
- topDocView.props.Document.subBulletDocs = undefined;
- } else {
- this.props.docs.map(d => d.removeTemplate(template));
- }
- this.props.templates.set(template, false);
+ this.props.docs.map(d => d.Document["show" + template.Name] = "");
}
}
@undoBatch
@action
clearTemplates = (event: React.MouseEvent) => {
- this.props.docs.map(d => d.clearTemplates());
- Array.from(this.props.templates.keys()).map(t => this.props.templates.set(t, false));
+ 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) {
+ 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;
+ }
+ }));
}
@action
- componentWillReceiveProps(nextProps: TemplateMenuProps) {
- // this._templates = nextProps.templates;
+ toggleTemplateActivity = (): void => {
+ this._hidden = !this._hidden;
}
+ @undoBatch
@action
- toggleTemplateActivity = (): void => {
- this._hidden = !this._hidden;
+ toggleChrome = (): void => {
+ this.props.docs.map(dv => {
+ let layout = dv.Document.layout instanceof Doc ? dv.Document.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 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={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
return (
<div className="templating-menu" >
<div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
<ul id="template-list" ref={this.dragRef} style={{ display: this._hidden ? "none" : "block" }}>
{templateMenu}
- <button onClick={this.toggleFloat}>Float</button>
- <button onClick={this.clearTemplates}>Clear</button>
+ {<button onClick={this.clearTemplates}>Restore Defaults</button>}
</ul>
</div>
);
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 236704fa2..ef78b60d4 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -57,43 +57,7 @@ export namespace Templates {
</div>
</div>` );
- export const Header = new Template("Header", TemplatePosition.InnerTop,
- `<div style = "display:flex; flex-direction:column; height:100%;" >
- <div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
- <FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} />
- </div>
- <div style="width:100%;height:100%;overflow:auto;">{layout}</div>
- </div > ` );
-
- export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
- `< div >
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
- <img id="isExpander" src="/assets/downarrow.png" width="15px" height="15px" />
- </div>
- </div > `
- );
-
- export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
- return (`< div >
- <div style="height:100%; width:100%; position:absolute;">{layout}</div>
- <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
- <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
- </div>
- </div > `);
- }
-
- export function TitleBar(datastring: string) {
- return (`<div>
- <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
- <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${datastring}</span>
- </div>
- <div style="height:calc(100% - 25px);">
- <div style="width:100%;overflow:auto">{layout}</div>
- </div>
- </div>` );
- }
- export const TemplateList: Template[] = [Title, Header, Caption, Bullet];
+ export const TemplateList: Template[] = [Title, Caption];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
index 583e6f6ca..aff965469 100644
--- a/src/client/views/collections/CollectionBaseView.scss
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -1,4 +1,5 @@
@import "../globalCssVariables";
+
#collectionBaseView {
border-width: 0;
border-color: $light-color-secondary;
@@ -6,7 +7,20 @@
border-radius: 0 0 $border-radius $border-radius;
box-sizing: border-box;
border-radius: inherit;
- width:100%;
- height:100%;
+ 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/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index b6ed6aaa0..62be1fc31 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,17 +1,18 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
+import { Doc, DocListCast } 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 { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } 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';
+import { ImageField } from '../../../new_fields/URLField';
export enum CollectionViewType {
Invalid,
@@ -20,7 +21,8 @@ export enum CollectionViewType {
Docking,
Tree,
Stacking,
- Masonry
+ Masonry,
+ Pivot,
}
export namespace CollectionViewType {
@@ -32,7 +34,8 @@ export namespace CollectionViewType {
["docking", CollectionViewType.Docking],
["tree", CollectionViewType.Tree],
["stacking", CollectionViewType.Stacking],
- ["masonry", CollectionViewType.Masonry]
+ ["masonry", CollectionViewType.Masonry],
+ ["pivot", CollectionViewType.Pivot]
]);
export const valueOf = (value: string) => {
@@ -79,12 +82,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
}
- @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+ @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 || BoolCast(this.props.Document.excludeFromLibrary);
+ return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
}
//TODO should this be observable?
@@ -94,16 +97,15 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
- @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+ @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;
+ var curTime = NumCast(this.props.Document.currentTimecode, -1);
+ curTime !== -1 && (doc.displayTimecode = curTime);
if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer
Doc.GetProto(doc).annotationOn = this.props.Document;
}
- allowDuplicates = true;
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));
@@ -126,9 +128,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
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[Id] === doc[Id]) ? i : p, -1);
- PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
- annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined));
+ 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 => {
+ if (Doc.AreProtosEqual(annotationOn, FieldValue(Cast(this.dataDoc.extendsDoc, Doc)))) {
+ Doc.GetProto(doc).annotationOn = undefined;
+ }
+ });
if (index !== -1) {
value.splice(index, 1);
@@ -140,17 +146,31 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
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 {
- let self = this;
- let targetDataDoc = this.props.Document;
- if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) {
+ if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
return true;
}
- if (this.removeDocument(doc)) {
- return addDocument(doc);
+ return this.removeDocument(doc) ? addDocument(doc) : false;
+ }
+
+ showIsTagged = () => {
+ const children = DocListCast(this.props.Document.data);
+ const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto);
+ const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags);
+ if (allTagged) {
+ return (
+ <img
+ id={"google-tags"}
+ src={"/assets/google_tags.png"}
+ />
+ );
}
- return false;
+ return (null);
}
render() {
@@ -170,6 +190,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}}
className={this.props.className || "collectionView-cont"}
onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
+ {this.showIsTagged()}
{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 0e7e0afa7..6f5abd05b 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,8 +1,5 @@
@import "../../views/globalCssVariables.scss";
-.collectiondockingview-content {
- height: 100%;
-}
.lm_active .messageCounter{
color:white;
background: #999999;
@@ -21,7 +18,7 @@
.collectiondockingview-container {
width: 100%;
- height: 100%;
+ height:100%;
border-style: solid;
border-width: $COLLECTION_BORDER_WIDTH;
position: absolute;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index dc0cbda0b..fe805a980 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,41 +1,43 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction, trace, computed } 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 } from "../../../new_fields/Doc";
import { Id } from '../../../new_fields/FieldSymbols';
+import { List } from '../../../new_fields/List';
import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
+import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { undoBatch } from "../../util/UndoManager";
+import { MainView } from '../MainView';
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
-import { ParentDocSelector } from './ParentDocumentSelector';
import React = require("react");
-import { MainView } from '../MainView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { Docs } from '../../documents/Documents';
-import { DateField } from '../../../new_fields/DateField';
-import { List } from '../../../new_fields/List';
+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 Instance: CollectionDockingView;
public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
return {
type: 'react-component',
@@ -44,13 +46,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
width: width,
props: {
documentId: document[Id],
- dataDocumentId: dataDoc ? dataDoc[Id] : ""
+ dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : ""
//collectionDockingView: CollectionDockingView.Instance
}
};
}
- 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 = "";
@@ -59,22 +65,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
constructor(props: SubCollectionViewProps) {
super(props);
- if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this;
+ !CollectionDockingView.Instance && runInAction(() => CollectionDockingView.Instance = this);
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
}
hack: boolean = false;
undohack: any = null;
- public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) {
+ public StartOtherDrag(e: any, dragDocs: Doc[]) {
let config: any;
if (dragDocs.length === 1) {
- config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]);
+ config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined);
} else {
config = {
type: 'row',
content: dragDocs.map((doc, i) => {
- CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]);
+ CollectionDockingView.makeDocumentConfig(doc, undefined);
})
};
}
@@ -84,18 +90,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
dragSource.destroy();
});
dragSource._dragListener.onMouseDown(e);
- // dragSource.destroy();
- // this.hack = true;
- // this.undohack = UndoManager.StartBatch("goldenDrag");
- // dragDocs.map((dragDoc, i) =>
- // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener.
- // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
}
+ @undoBatch
@action
public OpenFullScreen(docView: DocumentView) {
let document = Doc.MakeAlias(docView.props.Document);
- let dataDoc = docView.dataDoc;
+ let dataDoc = docView.props.DataDoc;
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
@@ -107,6 +108,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._maximizedSrc = docView;
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
+ SelectionManager.DeselectAll();
}
public CloseFullScreen = () => {
@@ -125,21 +127,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
@action
- public CloseRightSplit = (document: Doc): boolean => {
+ public static CloseRightSplit(document: Doc): boolean {
+ if (!CollectionDockingView.Instance) return false;
+ let instance = CollectionDockingView.Instance;
let retVal = false;
- if (this._goldenLayout.root.contentItems[0].isRow) {
- retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (instance._goldenLayout.root.contentItems[0].isRow) {
+ retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) &&
Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
child.contentItems[0].remove();
- this.layoutChanged(document);
+ instance.layoutChanged(document);
return true;
} else {
Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) &&
+ Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
child.contentItems[j].remove();
child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
- let docs = Cast(this.props.Document.data, listSpec(Doc));
+ let docs = Cast(instance.props.Document.data, listSpec(Doc));
docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
return true;
}
@@ -150,7 +156,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
});
}
if (retVal) {
- this.stateChanged();
+ instance.stateChanged();
}
return retVal;
}
@@ -175,9 +181,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
+ @undoBatch
@action
- public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => {
- let docs = Cast(this.props.Document.data, listSpec(Doc));
+ public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) {
+ if (!CollectionDockingView.Instance) return false;
+ let instance = CollectionDockingView.Instance;
+ let docs = Cast(instance.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
}
@@ -186,15 +195,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
};
- var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
+ var newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
- if (this._goldenLayout.root.contentItems.length === 0) {
- this._goldenLayout.root.addChild(newContentItem);
- } else if (this._goldenLayout.root.contentItems[0].isRow) {
- this._goldenLayout.root.contentItems[0].addChild(newContentItem);
+ if (instance._goldenLayout.root.contentItems.length === 0) {
+ instance._goldenLayout.root.addChild(newContentItem);
+ } else if (instance._goldenLayout.root.contentItems[0].isRow) {
+ instance._goldenLayout.root.contentItems[0].addChild(newContentItem);
} else {
- var collayout = this._goldenLayout.root.contentItems[0];
- var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout);
+ var collayout = instance._goldenLayout.root.contentItems[0];
+ var newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
collayout.parent.replaceChild(collayout, newRow);
newRow.addChild(newContentItem, undefined, true);
@@ -209,10 +218,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
// newContentItem.config.height = 10;
}
newContentItem.callDownwards('_$init');
- this.layoutChanged();
-
- return newContentItem;
+ instance.layoutChanged();
+ return true;
}
+
+ @undoBatch
@action
public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
Doc.GetProto(document).lastOpened = new DateField;
@@ -239,13 +249,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stack.addChild(docContentConfig, undefined);
}
this.layoutChanged();
+ return true;
}
setupGoldenLayout() {
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())) {
@@ -258,7 +269,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);
@@ -286,7 +297,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 = "";
@@ -306,7 +318,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
if (this._goldenLayout) this._goldenLayout.destroy();
- this._goldenLayout = null;
+ runInAction(() => this._goldenLayout = null);
window.removeEventListener('resize', this.onResize);
if (this.reactionDisposer) {
@@ -397,6 +409,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
dragSpan.style.bottom = "6px";
dragSpan.style.paddingLeft = "4px";
dragSpan.style.paddingRight = "2px";
+ let gearSpan = document.createElement("span");
+ gearSpan.style.position = "relative";
+ gearSpan.style.paddingLeft = "0px";
+ gearSpan.style.paddingRight = "12px";
let upDiv = document.createElement("span");
const stack = tab.contentItem.parent;
// shifts the focus to this tab when another tab is dragged over it
@@ -412,14 +428,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
e => {
e.preventDefault();
e.stopPropagation();
- DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, {
+ DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
handlers: { dragComplete: emptyFunction },
hideSource: false
});
}}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
- ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
- tab.reactComponents = [dragSpan, upDiv];
+ ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan);
+ // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => {
+ // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc);
+ // return true;
+ // }} />, upDiv);
+ tab.reactComponents = [dragSpan, gearSpan, upDiv];
tab.element.append(dragSpan);
+ tab.element.append(gearSpan);
tab.element.append(upDiv);
tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => {
tab.titleElement[0].textContent = doc.title, { fireImmediately: true };
@@ -438,8 +459,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();
@@ -471,12 +493,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;
@@ -520,20 +543,15 @@ interface DockedFrameProps {
}
@observer
export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
- _mainCont: HTMLDivElement | undefined = undefined;
+ _mainCont: HTMLDivElement | null = null;
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
@observable private _dataDoc: Opt<Doc>;
-
@observable private _isActive: boolean = false;
get _stack(): any {
- let parent = (this.props as any).glContainer.parent.parent;
- if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
- return parent.parent.contentItems[1];
- }
- return parent;
+ return (this.props as any).glContainer.parent.parent;
}
constructor(props: any) {
super(props);
@@ -554,11 +572,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");
@@ -567,6 +588,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
componentDidMount() {
+ let observer = new _global.ResizeObserver(action((entries: any) => {
+ for (let entry of entries) {
+ this._panelWidth = entry.contentRect.width;
+ this._panelHeight = entry.contentRect.height;
+ }
+ }));
+ observer.observe(this.props.glContainer._element[0]);
this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
this.props.glContainer.on("tab", this.onActiveContentItemChanged);
this.onActiveContentItemChanged();
@@ -585,13 +613,21 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
- panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
- panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight)));
+ panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth;
+ panelHeight = () => this._panelHeight;
- nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
+ 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;
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);
+ } else {
+ return this._panelHeight / NumCast(this._document!.nativeHeight);
+ }
+ }
const nativeH = this.nativeHeight();
const nativeW = this.nativeWidth();
if (!nativeW || !nativeH) return 1;
@@ -607,36 +643,38 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
return Transform.Identity();
}
- get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
+ get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
- addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
+ addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
+ SelectionManager.DeselectAll();
if (doc.dockingConfig) {
MainView.Instance.openWorkspace(doc);
+ return true;
} else if (location === "onRight") {
- CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
+ return CollectionDockingView.AddRightSplit(doc, dataDoc);
} else if (location === "close") {
- CollectionDockingView.Instance.CloseRightSplit(doc);
+ return CollectionDockingView.CloseRightSplit(doc);
} else {
- CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
+ return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
}
}
+
@computed get docView() {
- if (!this._document) {
- return (null);
- }
- let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
- return <DocumentView key={this._document[Id]}
- Document={this._document}
+ 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}
DataDoc={resolvedDataDoc}
bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
+ ruleProvider={undefined}
ContentScaling={this.contentScaling}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.ScreenToLocalTransform}
renderDepth={0}
- selectOnLoad={false}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
@@ -644,32 +682,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
addDocTab={this.addDocTab}
pinToPres={this.PinDoc}
ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
zoomToScale={emptyFunction}
getScale={returnOne} />;
}
- @computed get content() {
- return (
- <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => {
- this._mainCont = ref;
- if (ref) {
- this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", ""));
- this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", ""));
- }
- })}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
+ render() {
+ return (!this._isActive || !this._document) ? (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%"
+ }}>
{this.docView}
</div >);
}
-
- render() {
- if (!this._isActive || !this._document) return null;
- let theContent = this.content;
- return !this._document ? (null) :
- <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
- {({ measureRef }) => <div ref={measureRef}>
- {theContent}
- </div>}
- </Measure>;
- }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index f01a54001..b744ce4a5 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -214,7 +214,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
if (compiled.compiled) {
let scriptField = new ScriptField(compiled);
alias.viewSpecScript = scriptField;
- let dragData = new DragManager.DocumentDragData([alias], [alias.proto]);
+ let dragData = new DragManager.DocumentDragData([alias]);
DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
}
@@ -379,6 +379,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div > : (null);
const background = this._background; //to account for observables in Measure
const collapsed = this.collapsed;
+ let chromeStatus = this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus;
return (
<Measure offset onResize={this.handleResize}>
{({ measureRef }) => {
@@ -404,7 +405,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
{this.masonryChildren(this.props.docList)}
{this.props.parent.columnDragger}
</div>
- {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
<div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
style={{ width: style.columnWidth / style.numGroupColumns }}>
<EditableView {...newEditableViewProps} />
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index 50201bae8..62ec8a5be 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -1,26 +1,4 @@
-.collectionPdfView-buttonTray {
- top: 15px;
- left: 20px;
- position: relative;
- transform-origin: left top;
- position: absolute;
-}
-.collectionPdfView-thumb {
- width: 25px;
- height: 25px;
- transform-origin: left top;
- position: absolute;
- background: darkgray;
-}
-
-.collectionPdfView-slider {
- width: 25px;
- height: 25px;
- transform-origin: left top;
- position: absolute;
- background: lightgray;
-}
.collectionPdfView-cont {
width: 100%;
@@ -29,28 +7,5 @@
top: 0;
left: 0;
z-index: -1;
+ overflow: hidden !important;
}
-
-.collectionPdfView-cont-dragging {
- span {
- user-select: none;
- }
-}
-
-.collectionPdfView-backward {
- color: white;
- font-size: 24px;
- top: 0px;
- left: 0px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
-}
-
-.collectionPdfView-forward {
- color: white;
- font-size: 24px;
- top: 0px;
- left: 45px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 8eda4d9ee..cc8142ec0 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,10 +1,9 @@
-import { computed } from "mobx";
+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 { PDFBox } from "../nodes/PDFBox";
import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionPDFView.scss";
@@ -17,35 +16,18 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
}
- private _pdfBox?: PDFBox;
- private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef();
-
- @computed
- get uIButtons() {
- return (
- <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}>
- <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button>
- <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button>
- </div>
- );
- }
-
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" });
}
}
- setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; };
-
subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- return (<>
- <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} />
- {renderProps.active() ? this.uIButtons : (null)}
- </>);
+ return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />);
}
render() {
+ trace();
return (
<CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}>
{this.subView}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 551b485e7..179e44266 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -34,12 +34,12 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
fieldKey: string;
renderDepth: number;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
isFocused: boolean;
@@ -149,11 +149,12 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
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,
isSelected: returnFalse,
select: emptyFunction,
renderDepth: this.props.renderDepth + 1,
- selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
active: returnFalse,
@@ -172,7 +173,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
let onItemDown = (e: React.PointerEvent) => {
if (fieldIsDoc) {
SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document,
- this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
+ this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument,
+ this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
}
};
let onPointerEnter = (e: React.PointerEvent): void => {
@@ -215,7 +217,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
isEditingCallback={this.isEditingCallback}
display={"inline"}
contents={contents}
- height={Number(MAX_ROW_HEIGHT)}
+ height={"auto"}
+ maxHeight={Number(MAX_ROW_HEIGHT)}
GetValue={() => {
let field = props.Document[props.fieldKey];
if (Field.IsField(field)) {
@@ -235,13 +238,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
}}
OnFillDown={async (value: string) => {
- let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- if (!script.compiled) {
- return;
+ const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ if (script.compiled) {
+ DocListCast(this.props.Document[this.props.fieldKey]).
+ forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run));
}
- const run = script.run;
- const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
- val && val.forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, run));
}}
/>
</div >
@@ -300,7 +301,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
render() {
let reference = React.createRef<HTMLDivElement>();
let onItemDown = (e: React.PointerEvent) => {
- (!this.props.CollectionView.props.isSelected() ? undefined :
+ (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined :
SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
};
return (
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index ec40043cc..39abc41ec 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -201,12 +201,8 @@ export class MovableRow extends React.Component<MovableRowProps> {
@action
move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
let targetView = DocumentManager.Instance.getDocumentView(target);
- if (targetView) {
- let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document;
- if (targetContainingColl) {
- let targetContCollDoc = targetContainingColl.props.Document;
- return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc);
- }
+ if (targetView && targetView.props.ContainingCollectionDoc) {
+ return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
}
return doc !== target && this.props.removeDoc(doc) && addDoc(doc);
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 4008dea51..1ba35a52b 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -14,7 +14,7 @@ import { listSpec } from "../../../new_fields/Schema";
import { Docs, DocumentOptions } from "../../documents/Documents";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { Gateway } from "../../northstar/manager/Gateway";
-import { SetupDrag, DragManager } from "../../util/DragManager";
+import { DragManager } from "../../util/DragManager";
import { CompileScript, ts, Transformer } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss';
@@ -32,6 +32,7 @@ import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, Collection
import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { DocumentType } from "../../documents/DocumentTypes";
library.add(faCog, faPlus, faSortUp, faSortDown);
@@ -50,7 +51,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
@@ -161,9 +162,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
childDocs={this.childDocs}
renderDepth={this.props.renderDepth}
- width={this.previewWidth}
- height={this.previewHeight}
+ ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
+ 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}
@@ -194,6 +197,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
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}
@@ -243,8 +247,9 @@ export interface SchemaTableProps {
PanelHeight: () => number;
PanelWidth: () => number;
childDocs?: Doc[];
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
renderDepth: number;
deleteDocument: (document: Doc) => boolean;
@@ -252,7 +257,7 @@ export interface SchemaTableProps {
ScreenToLocalTransform: () => Transform;
active: () => boolean;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
isSelected: () => boolean;
isFocused: (document: Doc) => boolean;
@@ -298,19 +303,15 @@ 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 }[] {
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 }[]);
}
@@ -800,7 +801,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
csv.substring(0, csv.length - 1);
let dbName = StrCast(this.props.Document.title);
let res = await Gateway.Instance.PostSchema(csv, dbName);
- if (self.props.CollectionView.props.addDocument) {
+ if (self.props.CollectionView && self.props.CollectionView.props.addDocument) {
let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
if (schemaDoc) {
//self.props.CollectionView.props.addDocument(schemaDoc, false);
@@ -899,10 +900,13 @@ interface CollectionSchemaPreviewProps {
childDocs?: Doc[];
renderDepth: number;
fitToBox?: boolean;
- width: () => number;
- height: () => number;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ ruleProvider: Doc | undefined;
+ focus?: (doc: Doc) => void;
showOverlays?: (doc: Doc) => { title?: string, caption?: string };
CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionDoc?: Doc;
onClick?: ScriptField;
getTransform: () => Transform;
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -910,7 +914,7 @@ interface CollectionSchemaPreviewProps {
removeDocument: (document: Doc) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
setPreviewScript: (script: string) => void;
previewScript?: string;
@@ -920,12 +924,12 @@ interface CollectionSchemaPreviewProps {
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 get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); }
+ private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); }
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());
+ 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;
}
@@ -943,22 +947,21 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- let docDrag = de.data;
- let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } });
this.props.childDocs && this.props.childDocs.map(otherdoc => {
- let doc = docDrag.draggedDocuments[0];
let target = Doc.GetProto(otherdoc);
- target.layout = target.detailedLayout = Doc.MakeDelegate(doc);
- computed.compiled && (target.miniLayout = new ComputedField(computed));
+ 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 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());
- get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
+ get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
@action
onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.setPreviewScript(e.currentTarget.value);
@@ -980,37 +983,38 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
<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) : (
+ style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
+ {!this.props.Document || !this.props.PanelWidth ? (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()
+ height: this.props.PanelHeight(),
+ width: this.props.PanelWidth()
}}>
- <DocumentView
+ <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}
- selectOnLoad={false}
ContentScaling={this.contentScaling}
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
- focus={emptyFunction}
+ focus={this.props.focus || emptyFunction}
backgroundColor={returnEmptyString}
bringToFront={emptyFunction}
zoomToScale={emptyFunction}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index cf7da069c..7bf8348ff 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -12,7 +12,7 @@
.collectionMasonryView {
height: 100%;
width: 100%;
- position: absolute;
+ position: relative;
top: 0;
overflow-y: auto;
flex-wrap: wrap;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 9c6d5c52a..cde015152 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";
@@ -31,11 +31,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
_draggerRef = React.createRef<HTMLDivElement>();
@observable _heightMap = new Map<string, number>();
_heightDisposer?: IReactionDisposer;
- _childLayoutDisposer?: IReactionDisposer;
_sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
@observable private cursor: CursorProperty = "grab";
+ @observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
@computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
@@ -44,7 +44,7 @@ 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.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.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));
@@ -96,12 +96,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
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), undefined));
- });
- // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking and masonry views -eeng
+ // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
this._heightDisposer = reaction(() => {
if (BoolCast(this.props.Document.autoHeight)) {
let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
@@ -130,7 +125,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
}
componentWillUnmount() {
- this._childLayoutDisposer && this._childLayoutDisposer();
this._heightDisposer && this._heightDisposer();
this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@@ -149,7 +143,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
- @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : 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);
@@ -159,11 +153,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
DataDocument={dataDoc}
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}
+ 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}
@@ -180,13 +177,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
if (!d) return 0;
let nw = NumCast(d.nativeWidth);
let nh = NumCast(d.nativeHeight);
- if (!d.ignoreAspect && nw && nh) {
+ if (!d.ignoreAspect && !d.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);
return wid * aspect;
}
- return d[HeightSym]();
+ return d.fitWidth ? this.props.PanelHeight() - 2 * this.yMargin : d[HeightSym]();
}
columnDividerDown = (e: React.PointerEvent) => {
@@ -223,12 +220,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;
}
});
}
@@ -240,14 +239,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) => {
@@ -293,11 +292,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ if (!dref) return Transform.Identity();
+ let y = this._scroll; // required for document decorations to update when the text box container is scrolled
let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
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);
}
@@ -342,7 +343,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+ this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode");
}
onContextMenu = (e: React.MouseEvent): void => {
@@ -352,8 +353,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" });
subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
- subItems.push({ description: "Edit onChildClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onChildClick") });
ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
+
+ let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
}
@@ -364,21 +369,28 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
contents: "+ ADD A GROUP"
};
Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
-
- let sections = (this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc) : [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]);
+ 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} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ 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.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch
+ {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch
onChange={this.onToggle}
onClick={this.onToggle}
- defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'}
+ defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'}
checkedChildren="edit"
unCheckedChildren="view"
/> : null}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index fffa1dde5..cc7fe2aa1 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 } from "mobx";
+import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
@@ -82,25 +82,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
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 = () => this.getDocTransform(pair.layout as Doc, dref.current!);
- this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ 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} >
- {this.props.parent.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)}
+ {parent.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)}
</div>;
});
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
- let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
- let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.parent.props.ScreenToLocalTransform().
- translate(offset[0], offset[1]).
- scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
- }
-
getValue = (value: string): any => {
let parsed = parseInt(value);
if (!isNaN(parsed)) {
@@ -164,8 +155,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
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 });
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
+ let maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+ let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
+ newDoc.heading = heading;
return this.props.parent.props.addDocument(newDoc);
}
@@ -197,13 +191,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
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], [alias.proto]);
- DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+ alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
+ if (alias.viewSpecScript) {
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
}
e.stopPropagation();
@@ -324,7 +314,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
style={{
width: (style.columnWidth) /
((uniqueHeadings.length +
- ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.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.
@@ -364,34 +354,36 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</div>
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
+ let chromeStatus = this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus;
return (
- <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.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}
- {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.children(this.props.docList)}
- {singleColumn ? (null) : this.props.parent.columnDragger}
+ {
+ 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.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>
- {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
- </div> : null}
- </div>
}
- </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 38b2939d9..10937fba2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,12 +1,12 @@
-import { action, computed } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import * as rp from 'request-promise';
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { BoolCast, Cast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -22,6 +22,9 @@ import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
+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;
@@ -29,16 +32,20 @@ export interface CollectionViewProps extends FieldViewProps {
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;
}
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ruleProvider: Doc | undefined;
}
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) => { //used for stacking and masonry view
this.dropDisposer && this.dropDisposer();
if (ele) {
@@ -49,33 +56,35 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.createDropTarget(ele);
}
- @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+ 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))));
+ }
+ componentWillUnmount() {
+ this._childLayoutDisposer && this._childLayoutDisposer();
+ }
- get childDocs() {
- let self = this;
- //TODO tfs: This might not be what we want?
- //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
- let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- if (viewSpecScript) {
- let script = viewSpecScript.script;
- docs = docs.filter(d => {
- let res = script.run({ doc: d });
- if (res.success) {
- return res.result;
- }
- else {
- console.log(res.error);
- }
- });
- }
- return docs;
+ // 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.
+ // 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.
+ @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];
+ }
+
+
+ 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! }));
}
get childDocList() {
- //TODO tfs: This might not be what we want?
- //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
+ return Cast(this.dataField, listSpec(Doc));
+ }
+ get childDocs() {
+ let docs = DocListCast(this.dataField);
+ const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
}
@action
@@ -116,7 +125,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
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, undefined)
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc)
);
e.stopPropagation();
return true;
@@ -125,9 +134,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (de.data.dropAction || de.data.userDropAction) {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (de.data.moveDocument) {
- let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
- added = movedDocs.reduce((added: boolean, d) =>
- de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
+ 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
+ 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);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
@@ -143,7 +154,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;
@@ -183,6 +194,7 @@ 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 });
+ ImageUtils.ExtractExif(doc);
this.props.addDocument(doc, false);
return;
} else {
@@ -211,13 +223,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
@@ -253,13 +274,20 @@ 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.fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
+ doc && this.props.addDocument(doc);
+ });
}));
});
promises.push(prom);
}
}
+ if (text) {
+ this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
+ return;
+ }
if (promises.length) {
Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 990979109..ca0c321b7 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -8,11 +8,11 @@
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;
@@ -31,7 +31,7 @@
position: relative;
width: 15px;
color: $intermediate-color;
- margin-top: 4px;
+ margin-top: 3px;
transform: scale(1.3, 1.3);
}
@@ -51,7 +51,7 @@
.editableView-container-editing {
display: block;
text-overflow: ellipsis;
- font-size: 24px;
+ font-size: 1vw;
white-space: nowrap;
}
}
@@ -81,6 +81,9 @@
.treeViewItem-openRight {
display: none;
+ height: 17px;
+ background: gray;
+ width: 15px;
}
.treeViewItem-border {
@@ -95,15 +98,15 @@
.treeViewItem-openRight {
display: inline-block;
- height: 13px;
- margin-top: 2px;
- margin-left: 5px;
+ height: 17px;
+ background: #a8a7a7;
+ width: 15px;
// display: inline;
svg {
display: block;
padding: 0px;
- margin: 0px;
+ margin-left: 3px;
}
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 7e1aacd5d..882a0f144 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,12 +1,13 @@
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 { Docs, DocUtils } from '../../documents/Documents';
@@ -17,18 +18,16 @@ 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 { 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';
export interface TreeViewProps {
@@ -37,9 +36,10 @@ export interface TreeViewProps {
containingCollection: Doc;
renderDepth: number;
deleteDoc: (doc: Doc) => boolean;
+ ruleProvider: Doc | undefined;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
panelWidth: () => number;
panelHeight: () => number;
@@ -52,6 +52,7 @@ export interface TreeViewProps {
active: () => boolean;
showHeaderFields: () => boolean;
preventTreeViewOpen: boolean;
+ renderedIds: string[];
}
library.add(faTrashAlt);
@@ -80,7 +81,7 @@ 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 : this.props.document.defaultExpandedView ? StrCast(this.props.document.defaultExpandedView) : ""; }
@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; }
@@ -176,7 +177,7 @@ class TreeView extends React.Component<TreeViewProps> {
SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)}
OnFillDown={undoBatch((value: string) => {
Doc.GetProto(this.dataDoc)[key] = value;
- let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined;
+ 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];
return this.props.addDocument(doc);
@@ -199,6 +200,7 @@ class TreeView extends React.Component<TreeViewProps> {
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
}
ContextMenu.Instance.addItem({ description: "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" });
ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
e.stopPropagation();
e.preventDefault();
@@ -215,7 +217,7 @@ 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) {
@@ -247,8 +249,8 @@ class TreeView extends React.Component<TreeViewProps> {
}
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;
+ if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
+ return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
}
docHeight = () => {
let bounds = this.boundsOfCollectionDocument;
@@ -271,10 +273,12 @@ class TreeView extends React.Component<TreeViewProps> {
if (contents instanceof Doc || Cast(contents, listSpec(Doc))) {
let remDoc = (doc: Doc) => this.remove(doc, key);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending, true));
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
DocListCast(contents), this.props.treeViewId, doc, undefined, key, 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.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, doc[Id]]);
} else {
contentElement = <EditableView
key="editableView"
@@ -299,14 +303,15 @@ class TreeView extends React.Component<TreeViewProps> {
const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
if (expandKey !== undefined) {
let remDoc = (doc: Doc) => this.remove(doc, expandKey);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending, true));
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true);
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,
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.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}>
@@ -320,10 +325,12 @@ class TreeView extends React.Component<TreeViewProps> {
DataDocument={this.resolvedDataDoc}
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}
moveDocument={this.props.moveDocument}
@@ -340,7 +347,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderBullet() {
- return <div className="bullet" onClick={action(() => this.treeViewOpen = !this.treeViewOpen)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
+ return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
{<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
</div>;
}
@@ -365,15 +372,15 @@ class TreeView extends React.Component<TreeViewProps> {
})}>
{this.treeViewExpandedView}
</span>);
- let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
- <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
- <FontAwesomeIcon icon="angle-right" size="lg" />
- </div>);
+ let openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" />
+ </div>);
return <>
- <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
+ <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
style={{
+ color: this.props.document.isMinimized ? "red" : "black",
background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0",
+ fontWeight: this.props.document.search_string ? "bold" : undefined,
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
}} >
@@ -392,13 +399,13 @@ class TreeView extends React.Component<TreeViewProps> {
{this.renderTitle}
</div>
<div className="treeViewItem-border">
- {!this.treeViewOpen ? (null) : this.renderContent}
+ {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent}
</div>
</li>
</div>;
}
public static GetChildElements(
- docList: Doc[],
+ docs: Doc[],
treeViewId: string,
containingCollection: Doc,
dataDoc: Doc | undefined,
@@ -407,7 +414,7 @@ class TreeView extends React.Component<TreeViewProps> {
remove: ((doc: Doc) => boolean),
move: DragManager.MoveFunction,
dropAction: dropActionType,
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void,
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean,
pinToPres: (document: Doc) => void,
screenToLocalXf: () => Transform,
outerXf: () => { translateX: number, translateY: number },
@@ -415,45 +422,38 @@ class TreeView extends React.Component<TreeViewProps> {
panelWidth: () => number,
renderDepth: number,
showHeaderFields: () => boolean,
- preventTreeViewOpen: boolean
+ preventTreeViewOpen: boolean,
+ renderedIds: string[]
) {
- let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0);
- let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
+ const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
- let script = viewSpecScript.script;
- docs = docs.filter(d => {
- let res = script.run({ doc: d });
- if (res.success) {
- return res.result;
+ docs = docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
+ }
+
+ let ascending = Cast(containingCollection.sortAscending, "boolean", null);
+ if (ascending !== undefined) {
+ docs.sort(function (a, b): 1 | -1 {
+ let descA = ascending ? b : a;
+ let descB = ascending ? a : b;
+ let first = descA.title;
+ let second = descB.title;
+ // TODO find better way to sort how to sort..................
+ if (typeof first === 'number' && typeof second === 'number') {
+ return (first - second) > 0 ? 1 : -1;
+ }
+ if (typeof first === 'string' && typeof second === 'string') {
+ return first > second ? 1 : -1;
}
- else {
- console.log(res.error);
+ if (typeof first === 'boolean' && typeof second === 'boolean') {
+ // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load
+ // return Number(descA.x) > Number(descB.x) ? 1 : -1;
+ // }
+ return first > second ? 1 : -1;
}
+ return ascending ? 1 : -1;
});
}
- let descending = BoolCast(containingCollection.stackingHeadersSortDescending, true);
- docs.slice().sort(function (a, b): 1 | -1 {
- let descA = descending ? b : a;
- let descB = descending ? a : b;
- let first = descA[String(containingCollection.sectionFilter)];
- let second = descB[String(containingCollection.sectionFilter)];
- // TODO find better way to sort how to sort..................
- if (typeof first === 'number' && typeof second === 'number') {
- return (first - second) > 0 ? 1 : -1;
- }
- if (typeof first === 'string' && typeof second === 'string') {
- return first > second ? 1 : -1;
- }
- if (typeof first === 'boolean' && typeof second === 'boolean') {
- // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load
- // return Number(descA.x) > Number(descB.x) ? 1 : -1;
- // }
- return first > second ? 1 : -1;
- }
- return descending ? 1 : -1;
- });
-
let rowWidth = () => panelWidth() - 20;
return docs.map((child, i) => {
let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
@@ -484,6 +484,7 @@ class TreeView extends React.Component<TreeViewProps> {
dataDoc={pair.data}
containingCollection={containingCollection}
treeViewId={treeViewId}
+ ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc}
key={child[Id]}
indentDocument={indent}
renderDepth={renderDepth}
@@ -500,7 +501,8 @@ class TreeView extends React.Component<TreeViewProps> {
parentKey={key}
active={active}
showHeaderFields={showHeaderFields}
- preventTreeViewOpen={preventTreeViewOpen} />;
+ preventTreeViewOpen={preventTreeViewOpen}
+ renderedIds={renderedIds} />;
});
}
}
@@ -536,7 +538,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) { // excludeFromLibrary means this is the user document
+ if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) {
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();
@@ -551,8 +553,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
openNotifsCol = () => {
- if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) {
- CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined);
+ if (CollectionTreeView.NotifsCol) {
+ this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight");
}
}
@@ -584,27 +586,28 @@ 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, !BoolCast(this.props.Document.stackingHeadersSortDescending, true));
+ 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") }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()}
+ 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}
display={"block"}
- height={72}
+ maxHeight={72}
+ height={"auto"}
GetValue={() => StrCast(this.resolvedDataDoc.title)}
SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)}
OnFillDown={undoBatch((value: string) => {
Doc.GetProto(this.props.Document).title = value;
- let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined;
+ 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, !BoolCast(this.props.Document.stackingHeadersSortDescending, true));
+ 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)}
@@ -613,7 +616,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
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",
- BoolCast(this.props.Document.preventTreeViewOpen))
+ BoolCast(this.props.Document.preventTreeViewOpen), [])
}
</ul>
</div >
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 5185d9d0e..3d898b7de 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -21,7 +21,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
- let curTime = NumCast(this.props.Document.curPage);
+ let curTime = NumCast(this.props.Document.currentTimecode);
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>
@@ -85,7 +85,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
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));
+ this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333));
}
e.stopImmediatePropagation();
}
@@ -94,7 +94,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
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);
+ this._isclick < 10 && (this.props.Document.currentTimecode = 0);
}
setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 3a8253720..893763840 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,10 +1,9 @@
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 { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
@@ -13,11 +12,13 @@ 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 { ImageUtils } from '../../util/Import & Export/ImageUtils';
export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -50,18 +51,25 @@ export class CollectionView extends React.Component<FieldViewProps> {
this._reactionDisposer && this._reactionDisposer();
}
+ // bcz: Argh? What's the height of the collection chomes??
+ chromeHeight = () => {
+ return (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+ }
+
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} />);
+ case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} 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.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+ case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+ case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
- return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ this.props.Document.freeformLayoutEngine = undefined;
+ return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
}
return (null);
}
@@ -77,44 +85,46 @@ export class CollectionView extends React.Component<FieldViewProps> {
if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
return [(null), this.SubViewHelper(type, renderProps)];
}
- else {
- return [
- (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />),
- this.SubViewHelper(type, renderProps)
- ];
- }
+ return [
+ <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />,
+ 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
- let subItems: ContextMenuProps[] = [];
- subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" });
+ let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
+ let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
if (CollectionBaseView.InSafeMode()) {
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" });
subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" });
subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+ subItems.push({
+ description: "Stacking (AutoHeight)", event: () => {
+ this.props.Document.viewType = CollectionViewType.Stacking;
+ this.props.Document.autoHeight = true;
+ }, icon: "ellipsis-v"
+ });
subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
+ subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
switch (this.props.Document.viewType) {
case CollectionViewType.Freeform: {
- subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) });
- subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true });
+ subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
break;
}
}
- ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, 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 : [];
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" });
-
- let makes = ContextMenu.Instance.findByDescription("Make...");
- let makeItems: ContextMenuProps[] = makes && "subitems" in makes ? makes.subitems : [];
- makeItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
- !makes && ContextMenu.Instance.addItem({ description: "Make...", subitems: makeItems, icon: "hand-point-right" });
+ ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 6ed9b3253..72f3514b6 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -10,7 +10,6 @@ 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";
@@ -20,7 +19,6 @@ import { CollectionView } from "./CollectionView";
import "./CollectionViewChromes.scss";
import * as Autosuggest from 'react-autosuggest';
import KeyRestrictionRow from "./KeyRestrictionRow";
-import { Docs } from "../../documents/Documents";
const datepicker = require('js-datepicker');
interface CollectionViewChromeProps {
@@ -41,16 +39,46 @@ let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
//(!)?\(\(\(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"],
+ initialize: emptyFunction,
+ immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = 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...
+ title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
+ initialize: emptyFunction,
+ immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
+ };
+ _viewCommand = {
+ title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
+ immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; },
+ initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }
+ };
+ _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand];
+ _stacking_commands = [this._contentCommand, this._templateCommand];
+ _masonry_commands = [this._contentCommand, this._templateCommand];
+ _tree_commands = [];
+ private get _buttonizableCommands() {
+ switch (this.props.type) {
+ case CollectionViewType.Tree: return this._tree_commands;
+ case CollectionViewType.Stacking: return this._stacking_commands;
+ case CollectionViewType.Masonry: return this._stacking_commands;
+ case CollectionViewType.Freeform: return this._freeform_commands;
+ }
+ return [];
+ }
+ private _picker: any;
+ private _commandRef = React.createRef<HTMLInputElement>();
+ private _autosuggestRef = React.createRef<Autosuggest>();
+ @observable private _currentKey: string = "";
@observable private _viewSpecsOpen: boolean = false;
@observable private _dateWithinValue: string = "";
@observable private _dateValue: Date | string = "";
@observable private _keyRestrictions: [JSX.Element, string][] = [];
@observable private suggestions: string[] = [];
- _commandRef = React.createRef<HTMLInputElement>();
@computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
- private _picker: any;
-
getFilters = (script: string) => {
let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g;
let arr: any[] = re.exec(script);
@@ -186,14 +214,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
}
let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
- `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
- `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
- "return true";
+ `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
+ `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
+ "true";
- let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false });
- if (compiled.compiled) {
- this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
- }
+ this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name });
}
@action
@@ -215,30 +240,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
subChrome = () => {
switch (this.props.type) {
- case CollectionViewType.Stacking: return (
- <CollectionStackingViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type} />);
- case CollectionViewType.Schema: return (
- <CollectionSchemaViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type}
- />);
- case CollectionViewType.Tree: return (
- <CollectionTreeViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type}
- />);
- case CollectionViewType.Masonry: return (
- <CollectionStackingViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type} />);
- default:
- return null;
+ 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;
}
}
@@ -256,7 +262,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@observable private pivotKeyDisplay = this.pivotKey;
getPivotInput = () => {
- if (!this.document.usePivotLayout) {
+ if (StrCast(this.document.freeformLayoutEngine) !== "pivot") {
return (null);
}
return (<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -274,11 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action.bound
clearFilter = () => {
- let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false });
- if (compiled.compiled) {
- this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
- }
-
+ this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name });
this._keyRestrictions = [];
this.addKeyRestrictions([]);
}
@@ -291,21 +293,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
}
-
- commands = [{
- // 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...
- title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
- immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
- },
- {
- title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"],
- immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined
- }];
@undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
if (de.data instanceof DragManager.DocumentDragData && de.data.draggedDocuments.length) {
- this.commands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments));
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments));
e.stopPropagation();
}
return true;
@@ -324,8 +316,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
}
}
- @observable private _currentKey: string = "";
- private autosuggestRef = React.createRef<Autosuggest>();
renderSuggestion = (suggestion: string) => {
return <p>{suggestion}</p>;
@@ -345,25 +335,47 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
this.suggestions = [];
}
getKeySuggestions = async (value: string): Promise<string[]> => {
- return this.commands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title);
+ return this._buttonizableCommands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title);
}
autoSuggestDown = (e: React.PointerEvent) => {
e.stopPropagation();
}
+ private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ private _sensitivity: number = 16;
+
dragCommandDown = (e: React.PointerEvent) => {
- this.commands.filter(c => c.title === this._currentKey).map(c =>
- DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
- { target: this.props.CollectionView.props.Document }, c.params, e.clientX, e.clientY));
+
+ this._startDragPosition = { x: e.clientX, y: e.clientY };
+ document.addEventListener("pointermove", this.dragPointerMove);
+ document.addEventListener("pointerup", this.dragPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragPointerMove = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
+ let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y];
+ if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c =>
+ DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
+ { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY));
+ document.removeEventListener("pointermove", this.dragPointerMove);
+ document.removeEventListener("pointerup", this.dragPointerUp);
+ }
+ }
+ dragPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragPointerMove);
+ document.removeEventListener("pointerup", this.dragPointerUp);
+
}
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"
@@ -386,6 +398,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
<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>
</select>
<div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -444,7 +457,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
renderSuggestion={this.renderSuggestion}
onSuggestionsFetchRequested={this.onSuggestionFetch}
onSuggestionsClearRequested={this.onSuggestionClear}
- ref={this.autosuggestRef} />
+ ref={this._autosuggestRef} />
</div>
</div>
</div>
@@ -515,18 +528,13 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
render() {
return (
<div className="collectionStackingViewChrome-cont">
- <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}>
- <div className="collectionStackingViewChrome-sortLabel">
- Sort
- </div>
- <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
- <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
- </div>
- </button>
<div className="collectionStackingViewChrome-sectionFilter-cont">
<div className="collectionStackingViewChrome-sectionFilter-label">
GROUP ITEMS BY:
- </div>
+ </div>
+ <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
<div className="collectionStackingViewChrome-sectionFilter">
<EditableView
GetValue={() => this.sectionFilter}
@@ -632,7 +640,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
@observable private _currentKey: string = "";
@observable private suggestions: string[] = [];
- @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
+ @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[]> => {
@@ -680,7 +688,11 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
return true;
}
- @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; };
+ @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() {
@@ -690,42 +702,10 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
<div className="collectionTreeViewChrome-sortLabel">
Sort
</div>
- <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}>
<FontAwesomeIcon icon="caret-up" size="2x" color="white" />
</div>
</button>
- <div className="collectionTreeViewChrome-sectionFilter-cont">
- <div className="collectionTreeViewChrome-sectionFilter-label">
- GROUP ITEMS BY:
- </div>
- <div className="collectionTreeViewChrome-sectionFilter">
- <EditableView
- GetValue={() => this.sectionFilter}
- autosuggestProps={
- {
- resetValue: this.resetValue,
- value: this._currentKey,
- onChange: this.onKeyChange,
- autosuggestProps: {
- inputProps:
- {
- value: this._currentKey,
- onChange: this.onKeyChange
- },
- getSuggestionValue: this.getSuggestionValue,
- suggestions: this.suggestions,
- alwaysRenderSuggestions: true,
- renderSuggestion: this.renderSuggestion,
- onSuggestionsFetchRequested: this.onSuggestionFetch,
- onSuggestionsClearRequested: this.onSuggestionClear
- }
- }}
- oneLine
- SetValue={this.setValue}
- contents={this.sectionFilter ? this.sectionFilter : "N/A"}
- />
- </div>
- </div>
</div>
);
}
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
index 2dd3e49f2..c186d15f8 100644
--- a/src/client/views/collections/ParentDocumentSelector.scss
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -19,4 +19,13 @@
border-right: 0px;
border-left: 0px;
}
+}
+.parentDocumentSelector-button {
+ pointer-events: all;
+}
+.buttonSelector {
+ position: absolute;
+ display: inline-block;
+ padding-left: 5px;
+ padding-right: 5px;
} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 17111af58..7f2913214 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -8,8 +8,15 @@ import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
import { NumCast } from "../../../new_fields/Types";
import { CollectionViewType } from "./CollectionBaseView";
+import { DocumentButtonBar } from "../DocumentButtonBar";
+import { DocumentManager } from "../../util/DocumentManager";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faEdit } from "@fortawesome/free-solid-svg-icons";
+import { library } from "@fortawesome/fontawesome-svg-core";
-type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
+library.add(faEdit);
+
+type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
@observer
export class SelectorContextMenu extends React.Component<SelectorProps> {
@observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -38,8 +45,8 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
return () => {
col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
- const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
col.panX = newPanX;
col.panY = newPanY;
}
@@ -83,7 +90,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
);
}
return (
- <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
+ <span className="parentDocumentSelector-button" style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}>
<p>^</p>
@@ -92,3 +99,38 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
);
}
}
+
+@observer
+export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> {
+ @observable hover = false;
+
+ @action
+ onMouseLeave = () => {
+ this.hover = false;
+ }
+
+ @action
+ onMouseEnter = () => {
+ this.hover = true;
+ }
+
+ render() {
+ let flyout;
+ if (this.hover) {
+ let view = DocumentManager.Instance.getDocumentView(this.props.Document);
+ flyout = !view ? (null) : (
+ <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}>
+ <DocumentButtonBar views={[view]} stack={this.props.Stack} />
+ </div>
+ );
+ }
+ return (
+ <span className="buttonSelector"
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ {this.hover ? (null) : <FontAwesomeIcon icon={faEdit} size={"sm"} />}
+ {flyout}
+ </span>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
new file mode 100644
index 000000000..886692172
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -0,0 +1,117 @@
+import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
+import { ScriptBox } from "../../ScriptBox";
+import { CompileScript } from "../../../util/Scripting";
+import { ScriptField } from "../../../../new_fields/ScriptField";
+import { OverlayView, OverlayElementOptions } from "../../OverlayView";
+import { emptyFunction } from "../../../../Utils";
+import React = require("react");
+
+interface PivotData {
+ type: string;
+ text: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ fontSize: number;
+}
+
+export interface ViewDefBounds {
+ x: number;
+ y: number;
+ z?: number;
+ width: number;
+ height: number;
+ transition?: string;
+}
+
+export interface ViewDefResult {
+ ele: JSX.Element;
+ bounds?: ViewDefBounds;
+}
+
+export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
+ let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+ const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
+ const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+
+ for (const doc of childDocs) {
+ const val = doc[StrCast(pivotDoc.pivotField, "title")];
+ if (val) {
+ !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
+ pivotColumnGroups.get(val)!.push(doc);
+ }
+ }
+
+ const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity);
+ const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
+ const docMap = new Map<Doc, ViewDefBounds>();
+ const groupNames: PivotData[] = [];
+
+ let x = 0;
+ pivotColumnGroups.forEach((val, key) => {
+ let y = 0;
+ let xCount = 0;
+ groupNames.push({
+ type: "text",
+ text: String(key),
+ x,
+ y: pivotAxisWidth + 50,
+ width: pivotAxisWidth * 1.25 * numCols,
+ height: 100,
+ fontSize: NumCast(pivotDoc.pivotFontSize, 10)
+ });
+ for (const doc of val) {
+ docMap.set(doc, {
+ x: x + xCount * pivotAxisWidth * 1.25,
+ y: -y,
+ width: pivotAxisWidth,
+ height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+ });
+ xCount++;
+ if (xCount >= numCols) {
+ xCount = 0;
+ y += pivotAxisWidth * 1.25;
+ }
+ }
+ x += pivotAxisWidth * 1.25 * (numCols + 1);
+ });
+
+ childPairs.map(pair => {
+ let defaultPosition = {
+ x: NumCast(pair.layout.x),
+ y: NumCast(pair.layout.y),
+ z: NumCast(pair.layout.z),
+ width: NumCast(pair.layout.width),
+ height: NumCast(pair.layout.height)
+ };
+ const pos = docMap.get(pair.layout) || defaultPosition;
+ layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
+ });
+ return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) };
+}
+
+export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
+ return () => {
+ let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+ let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
+ const scriptField = Cast(doc[key], ScriptField);
+ let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
+ onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below
+ onSave={(text, onError) => {
+ const script = CompileScript(text, { params, requiredType, typecheck: false });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ } else {
+ doc[key] = new ScriptField(script);
+ overlayDisposer();
+ }
+ }} />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
+ };
+ addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
+ addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+ };
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index fc5212edd..cfd18ad35 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,8 +1,9 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
transform: translate(10000px,10000px);
- opacity: 0.5;
+ opacity: 0.8;
pointer-events: all;
+ stroke-width: 3px;
}
.collectionfreeformlinkview-linkCircle {
stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 6af87b138..df089eb00 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -39,10 +39,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
// 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) / NumCast(a.zoomBasis, 1) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
+ 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) + ")" : "");
@@ -50,7 +50,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
return (
<>
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${2 * 1 / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
{/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 2d94f1b8e..a81f5315a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -31,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
// let srcTarg = srcDoc;
// let x1 = NumCast(srcDoc.x);
// let x2 = NumCast(dstDoc.x);
- // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
- // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ // let x1w = NumCast(srcDoc.width, -1);
+ // let x2w = NumCast(dstDoc.width, -1);
// if (x1w < 0 || x2w < 0 || i === j) { }
// else {
// let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
@@ -79,15 +79,15 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
if (containerDoc) {
equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
}
- if (view.props.ContainingCollectionView) {
- let collid = view.props.ContainingCollectionView.props.Document[Id];
+ 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.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
+ return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document);
}
@computed
@@ -120,9 +120,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
render() {
return (
<div className="collectionfreeformlinksview-container">
- {/* <svg className="collectionfreeformlinksview-svgCanvas">
+ <svg className="collectionfreeformlinksview-svgCanvas">
{this.uniqueConnections}
- </svg> */}
+ </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 0a2dcbe3b..38488f033 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,22 +1,23 @@
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 { action, computed, IReactionDisposer, observable, reaction } from "mobx";
+import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faFileUpload } from "@fortawesome/free-solid-svg-icons";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
+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 { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { DocumentType } from "../../../documents/DocumentTypes";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
-import { CompileScript } from "../../../util/Scripting";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
@@ -24,22 +25,21 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
-import { OverlayElementOptions, OverlayView } from "../../OverlayView";
import PDFMenu from "../../pdf/PDFMenu";
-import { ScriptBox } from "../../ScriptBox";
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 { DocServer } from "../../../DocServer";
-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",
@@ -47,245 +47,80 @@ export const panZoomSchema = createSchema({
scale: "number",
arrangeScript: ScriptField,
arrangeInit: ScriptField,
+ useClusters: "boolean",
+ isRuleProvider: "boolean",
+ fitToBox: "boolean",
+ panTransformType: "string",
});
-export interface ViewDefBounds {
- x: number;
- y: number;
- z?: number;
- width: number;
- height: number;
-}
-
-export interface ViewDefResult {
- ele: JSX.Element;
- bounds?: ViewDefBounds;
-}
-
-export namespace PivotView {
-
- export interface PivotData {
- type: string;
- text: string;
- x: number;
- y: number;
- width: number;
- height: number;
- fontSize: number;
- }
-
- export const elements = (target: CollectionFreeFormView) => {
- let collection = target.Document;
- const field = StrCast(collection.pivotField) || "title";
- const width = NumCast(collection.pivotWidth) || 200;
- const groups = new Map<FieldResult<Field>, Doc[]>();
-
- for (const doc of target.childDocs) {
- const val = doc[field];
- if (val === undefined) continue;
-
- const l = groups.get(val);
- if (l) {
- l.push(doc);
- } else {
- groups.set(val, [doc]);
- }
- }
-
- let minSize = Infinity;
-
- groups.forEach((val, key) => minSize = Math.min(minSize, val.length));
-
- const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize));
- const fontSize = NumCast(collection.pivotFontSize);
-
- const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];
-
- let x = 0;
- groups.forEach((val, key) => {
- let y = 0;
- let xCount = 0;
- groupNames.push({
- type: "text",
- text: String(key),
- x,
- y: width + 50,
- width: width * 1.25 * numCols,
- height: 100, fontSize: fontSize
- });
- for (const doc of val) {
- docMap.set(doc, {
- x: x + xCount * width * 1.25,
- y: -y,
- width,
- height: width
- });
- xCount++;
- if (xCount >= numCols) {
- xCount = 0;
- y += width * 1.25;
- }
- }
- x += width * 1.25 * (numCols + 1);
- });
-
- let elements = target.viewDefsToJSX(groupNames);
- let docViews = target.childDocs.reduce((prev, doc) => {
- let minim = BoolCast(doc.isMinimized);
- if (minim === undefined || !minim) {
- let defaultPosition = (): ViewDefBounds => {
- return {
- x: NumCast(doc.x),
- y: NumCast(doc.y),
- z: NumCast(doc.z),
- width: NumCast(doc.width),
- height: NumCast(doc.height)
- };
- };
- const pos = docMap.get(doc) || defaultPosition();
- prev.push({
- ele: <CollectionFreeFormDocumentView
- key={doc[Id]}
- x={pos.x}
- y={pos.y}
- width={pos.width}
- height={pos.height}
- {...target.getChildDocumentViewProps(doc)}
- />,
- bounds: {
- x: pos.x,
- y: pos.y,
- z: pos.z,
- width: NumCast(pos.width),
- height: NumCast(pos.height)
- }
- });
- }
- return prev;
- }, elements);
-
- target.resetSelectOnLoaded();
-
- return docViews;
- };
-
-}
-
-type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
-const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
@observer
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
- private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
private _lastX: number = 0;
private _lastY: number = 0;
- private get _pwidth() { return this.props.PanelWidth(); }
- private get _pheight() { return this.props.PanelHeight(); }
- private inkKey = "ink";
- private _childLayoutDisposer?: IReactionDisposer;
-
- componentDidMount() {
- this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
- async (args) => {
- this.childDocs.filter(doc => args[1] instanceof Doc || doc.layout instanceof Doc).map(async doc => {
- if (!Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc)) {
- Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined);
- }
- });
- });
- }
- componentWillUnmount() {
- this._childLayoutDisposer && this._childLayoutDisposer();
- }
-
- get parentScaling() {
- return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
- }
-
- ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
- let bounds = boundsList.reduce((bounds, b) => {
- var [sptX, sptY] = [b.x, b.y];
- let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)];
- return {
- x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
- r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- };
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
- return bounds;
- }
-
- @computed get contentBounds() {
- let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined;
- let res = {
- panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,
- panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0,
- scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling
- };
- if (res.scale === 0) res.scale = 1;
- return res;
- }
-
- @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; }
- @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }
- public 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 _clusterDistance: number = 75;
+ private _hitCluster = false;
+ @observable _clusterSets: (Doc[])[] = [];
+
+ @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 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 borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private panX = () => this.contentBounds.panX;
- private panY = () => this.contentBounds.panY;
- private zoomScaling = () => this.contentBounds.scale;
- private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
- private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections
+ 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)
+ 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());
private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1);
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
- this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+ let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0;
+ if (heading === 0) {
+ let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
+ DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0);
+ 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);
}
private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
- this.props.addDocument(newBox, false);
- this.bringToFront(newBox);
- this.updateClusters();
- return true;
+ let added = this.props.addDocument(newBox, false);
+ added && this.bringToFront(newBox);
+ added && this.updateCluster(newBox);
+ return added;
}
private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll();
- docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
- SelectionManager.SelectDoc(dv!, true));
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
}
+ public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+
public getActiveDocuments = () => {
- const curPage = FieldValue(this.Document.curPage, -1);
- return this.childDocs.filter(doc => {
- var page = NumCast(doc.page, -1);
- return page === curPage || page === -1;
- });
+ return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
}
@computed get fieldExtensionDoc() {
- return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
- }
-
- intersectRect(r1: { left: number, top: number, width: number, height: number },
- r2: { left: number, top: number, width: number, height: number }) {
- return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
- }
- _clusterDistance = 75;
- boundsOverlap(doc: Doc, doc2: Doc) {
- var x2 = NumCast(doc2.x) - this._clusterDistance;
- var y2 = NumCast(doc2.y) - this._clusterDistance;
- var w2 = NumCast(doc2.width) + this._clusterDistance;
- var h2 = NumCast(doc2.height) + this._clusterDistance;
- var x = NumCast(doc.x) - this._clusterDistance;
- var y = NumCast(doc.y) - this._clusterDistance;
- var w = NumCast(doc.width) + this._clusterDistance;
- var h = NumCast(doc.height) + this._clusterDistance;
- if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
- return true;
- }
- return false;
+ return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
+ }
+
+ @action
+ onDrop = (e: React.DragEvent): Promise<void> => {
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ return super.onDrop(e, { x: pt[0], y: pt[1] });
}
+
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
@@ -297,11 +132,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
let z = NumCast(de.data.droppedDocuments[0].z);
- let x = (z ? xpo : xp) - de.data.xOffset;
- let y = (z ? ypo : yp) - de.data.yOffset;
+ 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);
- de.data.droppedDocuments.forEach(d => {
+ de.data.droppedDocuments.forEach(action((d: Doc) => {
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
if (!NumCast(d.width)) {
@@ -313,16 +148,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
}
this.bringToFront(d);
- });
+ }));
- this.updateClusters();
+ de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
}
}
else if (de.data instanceof DragManager.AnnotationDragData) {
if (de.data.dropDocument) {
let dragDoc = de.data.dropDocument;
- let x = xp - de.data.xOffset;
- let y = yp - de.data.yOffset;
+ 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);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
@@ -336,20 +171,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
- tryDragCluster(e: PointerEvent) {
- let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
- let cluster = this.childDocs.reduce((cluster, cd) => {
+ pickCluster(probe: number[]) {
+ return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, 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;
- if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) {
- return NumCast(cd.cluster);
- }
- return cluster;
+ return !cd.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);
+ }
+ tryDragCluster(e: PointerEvent) {
+ let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
if (cluster !== -1) {
- let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+ 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();
@@ -358,13 +193,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
SelectionManager.DeselectAll();
prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
- let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
+ 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);
- const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
+ de.offset = this.getTransform().transformDirection(e.x - left, e.y - top);
de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
- de.xOffset = xoff;
- de.yOffset = yoff;
DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
handlers: { dragComplete: action(emptyFunction) },
hideSource: !de.dropAction
@@ -374,56 +207,68 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
- @observable sets: (Doc[])[] = [];
+
+ @undoBatch
+ updateClusters(useClusters: boolean) {
+ this.props.Document.useClusters = useClusters;
+ this._clusterSets.length = 0;
+ this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
+ }
+
+ @undoBatch
@action
- updateClusters() {
- this.sets.length = 0;
- this.childDocs.map(c => {
- let included = [];
- for (let i = 0; i < this.sets.length; i++) {
- for (let member of this.sets[i]) {
- if (this.boundsOverlap(c, member)) {
- included.push(i);
- break;
- }
+ updateCluster(doc: Doc) {
+ let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
+ if (this.props.Document.useClusters) {
+ this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
+ let preferredInd = NumCast(doc.cluster);
+ doc.cluster = -1;
+ this._clusterSets.map((set, i) => set.map(member => {
+ if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+ doc.cluster = i;
}
+ }));
+ if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+ doc.cluster = preferredInd;
}
- if (included.length === 0) {
- this.sets.push([c]);
- } else if (included.length === 1) {
- this.sets[included[0]].push(c);
- } else {
- this.sets[included[0]].push(c);
- for (let s = 1; s < included.length; s++) {
- this.sets[included[0]].push(...this.sets[included[s]]);
- this.sets[included[s]].length = 0;
+ this._clusterSets.map((set, i) => {
+ if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
+ doc.cluster = i;
}
+ });
+ if (doc.cluster === -1) {
+ doc.cluster = this._clusterSets.length;
+ this._clusterSets.push([doc]);
+ } else {
+ for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ this._clusterSets[doc.cluster].push(doc);
}
- });
- this.sets.map((set, i) => set.map(member => member.cluster = i));
+ }
}
getClusterColor = (doc: Doc) => {
- if (this.props.Document.useClusters) {
- let cluster = NumCast(doc.cluster);
- if (this.sets.length <= cluster) {
- setTimeout(() => this.updateClusters(), 0);
- return;
+ let clusterColor = "";
+ let cluster = NumCast(doc.cluster);
+ if (this.Document.useClusters) {
+ if (this._clusterSets.length <= cluster) {
+ setTimeout(() => this.updateCluster(doc), 0);
+ } else {
+ // choose a cluster color from a palette
+ let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+ clusterColor = colors[cluster % colors.length];
+ let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
+ // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document
+ set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
+ set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
}
- let set = this.sets.length > cluster ? this.sets[cluster] : undefined;
- let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
- let clusterColor = colors[cluster % colors.length];
- set && set.filter(s => !s.isBackground).map(s =>
- s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
- set && set.filter(s => s.isBackground).map(s =>
- s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
- return clusterColor;
}
- return "";
+ return clusterColor;
}
@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()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -441,8 +286,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble) {
- if (this.props.Document.useClusters && this.tryDragCluster(e)) {
+ if (!e.cancelBubble && !this.isAnnotationOverlay) {
+ 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();
document.removeEventListener("pointermove", this.onPointerMove);
@@ -451,7 +296,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
let x = this.Document.panX || 0;
let y = this.Document.panY || 0;
- let docs = this.childDocs || [];
+ 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);
@@ -476,14 +321,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
});
}
- let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(),
- this._pheight / this.zoomScaling());
- let panelwidth = panelDim[0];
- let panelheight = panelDim[1];
- if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
- if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
- if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
- if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
+ let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
+ let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
+ this.props.PanelHeight() / this.zoomScaling() * cscale);
+ if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -495,34 +339,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (BoolCast(this.props.Document.lockedPosition)) return;
+ if (this.props.Document.lockedPosition || this.props.Document.inOverlay || this.isAnnotationOverlay) return;
if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
e.stopPropagation();
- return;
- }
-
- let childSelected = this.childDocs.some(doc => {
- var dv = DocumentManager.Instance.getDocumentView(doc);
- return dv && SelectionManager.IsSelected(dv) ? true : false;
- });
- if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) {
- return;
}
- e.stopPropagation();
-
- // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults. need more logic to fix.
- // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) {
- // let deltaScale = (1 - (e.deltaY / coefficient));
- // let nw = this.nativeWidth * deltaScale;
- // let nh = this.nativeHeight * deltaScale;
- // if (nw && nh) {
- // this.props.Document.nativeWidth = nw;
- // this.props.Document.nativeHeight = nh;
- // }
- // e.preventDefault();
- // }
- // else
- {
+ else if (this.props.active()) {
+ e.stopPropagation();
let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
deltaScale = 1 / this.zoomScaling();
@@ -534,13 +356,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
this.props.Document.scale = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
- e.preventDefault();
}
}
@action
setPan(panX: number, panY: number) {
- if (!BoolCast(this.props.Document.lockedPosition)) {
+ if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) {
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
@@ -550,125 +371,109 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
- @action
- onDrop = (e: React.DragEvent): void => {
- var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- super.onDrop(e, { x: pt[0], y: pt[1] });
- }
-
- onDragOver = (): void => {
- }
-
bringToFront = (doc: Doc, sendToBack?: boolean) => {
if (sendToBack || doc.isBackground) {
doc.zIndex = 0;
- return;
}
- const docs = this.childDocs;
- docs.slice().sort((doc1, doc2) => {
- if (doc1 === doc) return 1;
- if (doc2 === doc) return -1;
- return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
- }).forEach((doc, index) => doc.zIndex = index + 1);
- doc.zIndex = docs.length + 1;
- }
-
- focusDocument = (doc: Doc, willZoom: boolean, scale?: number) => {
- const panX = this.Document.panX;
- const panY = this.Document.panY;
- const id = this.Document[Id];
+ else {
+ const docs = this.childLayoutPairs.map(pair => pair.layout);
+ docs.slice().sort((doc1, doc2) => {
+ if (doc1 === doc) return 1;
+ if (doc2 === doc) return -1;
+ return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
+ }).forEach((doc, index) => doc.zIndex = index + 1);
+ doc.zIndex = docs.length + 1;
+ }
+ }
+
+ focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => {
const state = HistoryUtil.getState();
- state.initializers = state.initializers || {};
// TODO This technically isn't correct if type !== "doc", as
// currently nothing is done, but we should probably push a new state
- if (state.type === "doc" && panX !== undefined && panY !== undefined) {
- const init = state.initializers[id];
+ if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) {
+ const init = state.initializers![this.Document[Id]];
if (!init) {
- state.initializers[id] = {
- panX, panY
- };
+ state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY };
HistoryUtil.pushState(state);
- } else if (init.panX !== panX || init.panY !== panY) {
- init.panX = panX;
- init.panY = panY;
+ } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) {
+ init.panX = this.Document.panX;
+ init.panY = this.Document.panY;
HistoryUtil.pushState(state);
}
}
SelectionManager.DeselectAll();
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
- const newState = HistoryUtil.getState();
- (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY };
- HistoryUtil.pushState(newState);
- this.setPan(newPanX, newPanY);
-
- this.props.Document.panTransformType = "Ease";
- this.props.focus(this.props.Document);
- if (willZoom) {
- this.setScaleToZoom(doc, scale);
+ if (this.props.Document.scrollHeight) {
+ let annotOn = Cast(doc.annotationOn, Doc) as Doc;
+ 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;
+ 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";
+ Doc.BrushDoc(this.props.Document);
+ this.props.focus(this.props.Document);
+ willZoom && this.setScaleToZoom(doc, scale);
+
+ afterFocus && setTimeout(() => {
+ if (afterFocus && afterFocus()) {
+ this.Document.panX = savedState.px;
+ this.Document.panY = savedState.py;
+ this.Document.scale = savedState.s;
+ this.Document.panTransformType = savedState.pt;
+ }
+ }, 1000);
}
}
setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
- let p = this.props;
- let PanelHeight = p.PanelHeight();
- let panelWidth = p.PanelWidth();
-
- let docHeight = NumCast(doc.height);
- let docWidth = NumCast(doc.width);
- let targetHeight = scale * PanelHeight;
- let targetWidth = scale * panelWidth;
-
- let maxScaleX: number = targetWidth / docWidth;
- let maxScaleY: number = targetHeight / docHeight;
- let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
- this.Document.scale = maxApplicableScale;
+ this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
}
zoomToScale = (scale: number) => {
this.Document.scale = scale;
}
- getScale = () => this.Document.scale ? this.Document.scale : 1;
+ getScale = () => this.Document.scale || 1;
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,
- onClick: this.props.onClick,
+ 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,
renderDepth: this.props.renderDepth + 1,
- selectOnLoad: childLayout[Id] === this._selectOnLoaded,
PanelWidth: childLayout[WidthSym],
PanelHeight: childLayout[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
+ ContainingCollectionDoc: this.props.Document,
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,
- onClick: this.props.onClick,
+ ...this.props,
ScreenToLocalTransform: this.getTransform,
- renderDepth: this.props.renderDepth,
- selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
PanelWidth: layoutDoc[WidthSym],
PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
@@ -676,206 +481,202 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
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
};
}
- getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } {
- const result = script.script.run(params);
- if (!result.success) {
- return {};
+ 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);
+ if (result && result.success) {
+ return { ...result, transition: "transform 1s" };
}
- let doc = params.doc;
- return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result;
+ 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") };
}
viewDefsToJSX = (views: any[]) => {
- let elements: ViewDefResult[] = [];
- if (Array.isArray(views)) {
- elements = views.reduce<typeof elements>((prev, ele) => {
- const jsx = this.viewDefToJSX(ele);
- jsx && prev.push(jsx);
- return prev;
- }, elements);
- }
- return elements;
+ return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
}
private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
if (viewDef.type === "text") {
- const text = Cast(viewDef.text, "string");
+ const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below
const x = Cast(viewDef.x, "number");
const y = Cast(viewDef.y, "number");
const z = Cast(viewDef.z, "number");
const width = Cast(viewDef.width, "number");
const height = Cast(viewDef.height, "number");
const fontSize = Cast(viewDef.fontSize, "number");
- if ([text, x, y, width, height].some(val => val === undefined)) {
- return undefined;
- }
-
- return {
- ele: <div className="collectionFreeform-customText" style={{
- transform: `translate(${x}px, ${y}px)`,
- width, height, fontSize
- }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
- };
+ return [text, x, y, width, height].some(val => val === undefined) ? undefined :
+ {
+ ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
+ {text}
+ </div>,
+ bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+ };
}
}
- @computed.struct
- get elements() {
- if (this.Document.usePivotLayout) return PivotView.elements(this);
- let curPage = FieldValue(this.Document.curPage, -1);
- const initScript = this.Document.arrangeInit;
- const script = this.Document.arrangeScript;
- let state: any = undefined;
- const docs = this.childDocs;
- let elements: ViewDefResult[] = [];
- if (initScript) {
- const initResult = initScript.script.run({ docs, collection: this.Document });
- if (initResult.success) {
- const result = initResult.result;
- const { state: scriptState, views } = result;
- state = scriptState;
- elements = this.viewDefsToJSX(views);
- }
+ lookupLayout = (doc: Doc, dataDoc?: Doc) => {
+ let data: any = undefined;
+ let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
+ switch (this.Document.freeformLayoutEngine) {
+ case "pivot": computedElementData = this.doPivotLayout; break;
+ default: computedElementData = this.doFreeformLayout; break;
}
- let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => {
- var page = NumCast(doc.page, -1);
- if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
- let minim = BoolCast(doc.isMinimized);
- if (minim === undefined || !minim) {
- const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) :
- { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") };
- state = pos.state === undefined ? state : pos.state;
- let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc);
- if (pair.layout && !(pair.data instanceof Promise)) {
- prev.push({
- ele: <CollectionFreeFormDocumentView key={doc[Id]}
- x={script ? pos.x : undefined} y={script ? pos.y : undefined}
- width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
- bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined
- });
- }
- }
+ computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => {
+ if (key.layout === doc && key.data === dataDoc) {
+ data = value;
}
- return prev;
- }, elements);
-
- this.resetSelectOnLoaded();
+ });
+ return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition };
+ }
- return docviews;
+ @computed
+ get doPivotLayout() {
+ return computePivotLayout(this.props.Document, this.childDocs,
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
}
- resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
+ @computed
+ get doFreeformLayout() {
+ let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+ let layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
+ const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
+ let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
+ let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
- @computed.struct
- get views() {
- let source = this.elements;
- return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
+ const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
+ state = pos.state === undefined ? state : pos.state;
+ layoutPoolData.set(pair, pos);
+ });
+ return { map: layoutPoolData, elements: elements };
}
- @computed.struct
- get overlayViews() {
- return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
+
+ @computed
+ get doLayoutComputation() {
+ let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
+ switch (this.Document.freeformLayoutEngine) {
+ case "pivot": computedElementData = this.doPivotLayout; break;
+ default: computedElementData = this.doFreeformLayout; break;
+ }
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
+ computedElementData.elements.push({
+ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout}
+ ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
+ jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+ bounds: this.lookupLayout(pair.layout, pair.data)
+ }));
+
+ return computedElementData;
}
+ @computed.struct get elements() { return this.doLayoutComputation.elements; }
+ @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
+ @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); }
@action
onCursorMove = (e: React.PointerEvent) => {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox;
-
- arrangeContents = async () => {
- const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+ layoutDocsInGrid = () => {
UndoManager.RunInBatch(() => {
- if (docs) {
- let startX = this.Document.panX || 0;
- let x = startX;
- let y = this.Document.panY || 0;
- let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc.width)));
- const height = Math.max(...docs.map(doc => NumCast(doc.height)));
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- x += width + 20;
- if (++i === 6) {
- i = 0;
- x = startX;
- y += height + 20;
- }
+ const docs = DocListCast(this.Document[this.props.fieldKey]);
+ let startX = this.Document.panX || 0;
+ let x = startX;
+ let y = this.Document.panY || 0;
+ let i = 0;
+ const width = Math.max(...docs.map(doc => NumCast(doc.width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ x += width + 20;
+ if (++i === 6) {
+ i = 0;
+ x = startX;
+ y += height + 20;
}
}
}, "arrange contents");
}
+ autoFormat = () => {
+ this.Document.isRuleProvider = !this.Document.isRuleProvider;
+ // 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 => {
+ 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) {
+ Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
+ }
+ })
+ );
+ }
+
analyzeStrokes = async () => {
- let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
- if (!data) {
- return;
+ let data = Cast(this.fieldExtensionDoc.ink, InkField);
+ if (data) {
+ CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
}
- let relevantKeys = ["inkAnalysis", "handwriting"];
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData);
}
onContextMenu = (e: React.MouseEvent) => {
let layoutItems: ContextMenuProps[] = [];
+
+ if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
+ layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
+ }
+ layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
+ layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" });
+ layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
+ layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+ layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
layoutItems.push({
- description: "Import document", icon: "upload", event: () => {
+ description: "Import document", icon: "upload", event: ({ x, y }) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
input.onchange = async _e => {
- const files = input.files;
- if (!files) return;
- const file = files[0];
- let formData = new FormData();
- formData.append('file', file);
- formData.append('remap', "true");
const upload = Utils.prepend("/uploadDoc");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json === "error") {
- return;
- }
- const doc = await DocServer.GetRefField(json);
- if (!doc || !(doc instanceof Doc)) {
- return;
+ let formData = new FormData();
+ const file = input.files && input.files[0];
+ if (file) {
+ formData.append('file', file);
+ formData.append('remap', "true");
+ const response = await fetch(upload, { method: "POST", body: formData });
+ const json = await response.json();
+ if (json !== "error") {
+ const doc = await DocServer.GetRefField(json);
+ 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);
+ }
+ }
}
- const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY);
- doc.x = x, doc.y = y;
- this.props.addDocument &&
- this.props.addDocument(doc, false);
};
input.click();
}
});
- layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
- layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
- layoutItems.push({
- description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
- event: async () => {
- Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
- Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
- this.props.Document.useClusters = !this.props.Document.useClusters;
- },
- icon: !this.props.Document.useClusters ? "braille" : "braille"
- });
+
layoutItems.push({
- description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
- event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
- icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
+ description: "Add Note ...",
+ subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
+ description: (i + 1) + ": " + StrCast(note.title),
+ event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
+ icon: "eye"
+ })) as ContextMenuProps[],
+ icon: "eye"
});
- layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
- layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
}
@@ -884,49 +685,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
]
- private overlayChildViews = () => {
- return [...this.overlayViews];
- }
-
- public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
- return () => {
- let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
- let overlayDisposer: () => void = emptyFunction;
- const script = Cast(doc[key], ScriptField);
- 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,
- requiredType,
- typecheck: false
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
- doc[key] = new ScriptField(script);
- overlayDisposer();
- }} />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
- };
- addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
- addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
- };
- }
-
render() {
- const easing = () => this.props.Document.panTransformType === "Ease";
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
+ // 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.
+ // 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 (
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}>
+ style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.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}
- getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
- easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ 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.childViews}
@@ -935,7 +711,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
- {this.overlayChildViews()}
+ {this.overlayViews}
<CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
@@ -944,24 +720,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@observer
class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- @computed get overlayView() {
- return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
- }
render() {
- return this.overlayView;
+ 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 }> {
- @computed get backgroundView() {
- let props = this.props;
- return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
- }
render() {
- return this.props.Document.backgroundLayout ? this.backgroundView : (null);
+ return !this.props.Document.backgroundLayout ? (null) :
+ (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
}
@@ -982,8 +752,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 27eafd769..eaf65b88c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,25 +1,24 @@
-import * as htmlToImage from "html-to-image";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, FieldResult } from "../../../../new_fields/Doc";
-import { Id } from "../../../../new_fields/FieldSymbols";
+import { Doc, DocListCast } from "../../../../new_fields/Doc";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
-import { Cast, NumCast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../../Utils";
-import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
-import { Templates } from "../../Templates";
import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
-import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -31,6 +30,8 @@ interface MarqueeViewProps {
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
+ isAnnotationOverlay: boolean;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@observer
@@ -44,6 +45,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@observable _visible: boolean = false;
_commandExecuted = false;
+ componentDidMount() {
+ this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor);
+ }
+
@action
cleanupInteractions = (all: boolean = false) => {
if (all) {
@@ -93,9 +98,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
});
} else if (!e.ctrlKey) {
- let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
- newBox.proto!.autoHeight = true;
- this.props.addLiveTextDocument(newBox);
+ this.props.addLiveTextDocument(
+ Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
+ } else if (e.keyCode > 48 && e.keyCode <= 57) {
+ let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
+ let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
+ text.layout = notes[(e.keyCode - 49) % notes.length];
+ this.props.addLiveTextDocument(text);
}
e.stopPropagation();
}
@@ -142,15 +151,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
@action
onPointerDown = (e: React.PointerEvent): void => {
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
- this._commandExecuted = false;
- PreviewCursor.Visible = false;
- this.cleanupInteractions(true);
+ this._downX = this._lastX = e.clientX;
+ this._downY = this._lastY = e.clientY;
if (e.button === 2 || (e.button === 0 && e.altKey)) {
- document.addEventListener("pointermove", this.onPointerMove, true);
- document.addEventListener("pointerup", this.onPointerUp, true);
- document.addEventListener("keydown", this.marqueeCommand, true);
+ this.setPreviewCursor(e.clientX, e.clientY, true);
if (e.altKey) {
//e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
e.preventDefault();
@@ -173,6 +177,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.stopPropagation();
e.preventDefault();
}
+ } else {
+ this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
}
if (e.altKey) {
e.preventDefault();
@@ -182,16 +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]);
- // console.log("pointer up!");
if (this._visible) {
- // console.log("visible");
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
}
this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
}
- //console.log("invisible");
this.cleanupInteractions(true);
if (e.altKey) {
@@ -199,11 +202,28 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
+ setPreviewCursor = (x: number, y: number, drag: boolean) => {
+ if (drag) {
+ this._downX = this._lastX = x;
+ this._downY = this._lastY = y;
+ this._commandExecuted = false;
+ PreviewCursor.Visible = false;
+ this.cleanupInteractions(true);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ } else {
+ this._downX = x;
+ this._downY = y;
+ PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+ }
+ }
+
@action
onClick = (e: React.MouseEvent): void => {
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+ this.setPreviewCursor(e.clientX, e.clientY, false);
// let the DocumentView stopPropagation of this event when it selects this document
} else { // why do we get a click event when the cursor have moved a big distance?
// let's cut it off here so no one else has to deal with it.
@@ -225,18 +245,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
}
- get ink() {
- let container = this.props.container.props.Document;
- let containerKey = this.props.container.props.fieldKey;
- let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
- return Cast(extensionDoc.ink, InkField);
+ 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);
}
set ink(value: InkField | undefined) {
- let container = Doc.GetProto(this.props.container.props.Document);
- let containerKey = this.props.container.props.fieldKey;
- let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
- extensionDoc.ink = value;
+ let cprops = this.props.container.props;
+ Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
}
@undoBatch
@@ -269,27 +285,46 @@ 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 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);
+ if (palette.indexOf(bg) !== -1) {
+ palette.splice(palette.indexOf(bg), 1);
+ if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
+ else usedPaletted.set(bg, 1);
+ }
+ });
+ usedPaletted.delete("#f1efeb");
+ usedPaletted.delete("white");
+ usedPaletted.delete("rgba(255,255,255,1)");
+ let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
+ let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
let inkData = this.ink ? this.ink.inkData : undefined;
let newCollection = Docs.Create.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
panX: 0,
panY: 0,
- backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
- defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
+ backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+ defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
width: bounds.width,
height: bounds.height,
- title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
+ title: "a nested collection",
});
let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
- if (e.key === "s") {
+ if (e.key === "s" || e.key === "S") {
selected.map(d => {
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
@@ -298,36 +333,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
return d;
});
newCollection.chromeStatus = "disabled";
- let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- newCollection.proto!.summaryDoc = summary;
- selected = [newCollection];
+ let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
newCollection.x = bounds.left + bounds.width;
- summary.proto!.subBulletDocs = new List<Doc>(selected);
- summary.templates = new List<string>([Templates.Bullet.Layout]);
- let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
- container.viewType = CollectionViewType.Stacking;
- container.autoHeight = true;
- this.props.addLiveTextDocument(container);
- // });
- } else if (e.key === "S") {
- selected.map(d => {
- 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;
- return d;
- });
- newCollection.chromeStatus = "disabled";
- let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- newCollection.proto!.summaryDoc = summary;
- selected = [newCollection];
- newCollection.x = bounds.left + bounds.width;
- //this.props.addDocument(newCollection, false);
- summary.proto!.summarizedDocs = new List<Doc>(selected);
- summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
- summary.autoHeight = true;
-
- this.props.addLiveTextDocument(summary);
+ Doc.GetProto(newCollection).summaryDoc = summary;
+ Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
+ if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
+ let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
+ container.viewType = CollectionViewType.Stacking;
+ container.autoHeight = true;
+ Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight"
+ this.props.addLiveTextDocument(container);
+ } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
+ Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ this.props.addLiveTextDocument(summary);
+ }
}
else {
this.props.addDocument(newCollection, false);
@@ -422,8 +442,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
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)` }} >
+ return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0} >
{this._visible ? this.marqueeDiv : null}
<div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
{this.props.children}
diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx
index 9931515f3..868afc423 100644
--- a/src/client/views/document_templates/image_card/ImageCard.tsx
+++ b/src/client/views/document_templates/image_card/ImageCard.tsx
@@ -1,8 +1,5 @@
import * as React from 'react';
-import { DocComponent } from '../../DocComponent';
import { FieldViewProps } from '../../nodes/FieldView';
-import { createSchema, makeInterface } from '../../../../new_fields/Schema';
-import { createInterface } from 'readline';
import { ImageBox } from '../../nodes/ImageBox';
export default class ImageCard extends React.Component<FieldViewProps> {
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index fc5f2410c..fc5f2410c 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ecb3e9db4..ecb3e9db4 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
new file mode 100644
index 000000000..9eeed1cc8
--- /dev/null
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -0,0 +1,93 @@
+@import "../globalCssVariables";
+
+.linkFollowBox-main {
+ position: absolute;
+ background: whitesmoke;
+ color: grey;
+ border-radius: 15px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ border: solid #BBBBBBBB 5px;
+ pointer-events: all;
+
+ .linkFollowBox-header {
+ height: 50px;
+ text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 16px;
+ width: 100%;
+ }
+
+ .direction-indicator {
+ font-size: 12px;
+ }
+
+ .closeDocument {
+ position: relative;
+ max-width: 30px;
+ top: -20px;
+ left: 460px;
+ color: $darker-alt-accent
+ }
+
+ .closeDocument:hover {
+ color: $main-accent;
+ }
+
+ .topHeader {
+ width: 100%;
+ height: 25px;
+ }
+
+ .linkFollowBox-footer {
+ height: 50px;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ button {
+ background-color: $darker-alt-accent;
+ width: 30%;
+ }
+ }
+
+ .linkFollowBox-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-column-gap: 5px;
+ margin-left: 5px;
+ margin-right: 5px;
+
+ .linkFollowBox-item {
+ background-color: $light-color;
+ width: 100%;
+ height: 100%;
+
+ .linkFollowBox-itemContent {
+ padding: 5px;
+ font-size: 12px;
+ overflow: scroll;
+
+ input[type=radio] {
+ border: 0px;
+ margin-right: 5px;
+ }
+ }
+
+ .title {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-transform: uppercase;
+ color: $light-color;
+ background-color: $lighter-alt-accent;
+ width: 100%;
+ height: 30px;
+ border-bottom: solid $darker-alt-accent 5px;
+ font-size: 12px;
+ text-align: center;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
new file mode 100644
index 000000000..b18aa5d63
--- /dev/null
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -0,0 +1,571 @@
+import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx";
+import React = require("react");
+import { observer } from "mobx-react";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { undoBatch } from "../../util/UndoManager";
+import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentView } from "../nodes/DocumentView";
+import "./LinkFollowBox.scss";
+import { SearchUtil } from "../../util/SearchUtil";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { listSpec } from "../../../new_fields/Schema";
+import { DocServer } from "../../DocServer";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { docs_v1 } from "googleapis";
+import { Utils } from "../../../Utils";
+import { Link } from "@react-pdf/renderer";
+
+enum FollowModes {
+ OPENTAB = "Open in Tab",
+ OPENRIGHT = "Open in Right Split",
+ OPENFULL = "Open Full Screen",
+ PAN = "Pan to Document",
+ INPLACE = "Open In Place"
+}
+
+enum FollowOptions {
+ ZOOM = "Zoom",
+ NOZOOM = "No Zoom",
+}
+
+@observer
+export class LinkFollowBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+ public static Instance: LinkFollowBox | undefined;
+ @observable static linkDoc: Doc | undefined = undefined;
+ @observable static destinationDoc: Doc | undefined = undefined;
+ @observable static sourceDoc: Doc | undefined = undefined;
+ @observable selectedMode: string = "";
+ @observable selectedContext: Doc | undefined = undefined;
+ @observable selectedContextAliases: Doc[] | undefined = undefined;
+ @observable selectedOption: string = "";
+ @observable selectedContextString: string = "";
+ @observable sourceView: DocumentView | undefined = undefined;
+ @observable canPan: boolean = false;
+ @observable shouldUseOnlyParentContext = false;
+ _contextDisposer?: IReactionDisposer;
+
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ LinkFollowBox.Instance = this;
+ this.resetVars();
+ this.props.Document.isBackground = true;
+ }
+
+ componentDidMount = () => {
+ this.resetVars();
+
+ this._contextDisposer = reaction(
+ () => this.selectedContextString,
+ async () => {
+ let ref = await DocServer.GetRefField(this.selectedContextString);
+ runInAction(() => {
+ if (ref instanceof Doc) {
+ this.selectedContext = ref;
+ }
+ });
+ if (this.selectedContext instanceof Doc) {
+ let aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
+ runInAction(() => { this.selectedContextAliases = aliases; });
+ }
+ }
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._contextDisposer && this._contextDisposer();
+ }
+
+ async resetPan() {
+ if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+ runInAction(() => this.canPan = false);
+ if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) {
+ let docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
+ let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
+
+ aliases.forEach(alias => {
+ if (docs.filter(doc => doc === alias).length > 0) {
+ runInAction(() => { this.canPan = true; });
+ }
+ });
+ }
+ }
+ }
+
+ @action
+ resetVars = () => {
+ this.selectedContext = undefined;
+ this.selectedContextString = "";
+ this.selectedMode = "";
+ this.selectedOption = "";
+ LinkFollowBox.linkDoc = undefined;
+ LinkFollowBox.sourceDoc = undefined;
+ LinkFollowBox.destinationDoc = undefined;
+ this.sourceView = undefined;
+ this.canPan = false;
+ this.shouldUseOnlyParentContext = false;
+ }
+
+ async fetchDocuments() {
+ if (LinkFollowBox.destinationDoc) {
+ let dest: Doc = LinkFollowBox.destinationDoc;
+ let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest));
+ const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` });
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(async () => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
+ runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
+ });
+ }
+ }
+
+ @action
+ setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => {
+ this.resetVars();
+
+ LinkFollowBox.linkDoc = linkDoc;
+ LinkFollowBox.sourceDoc = source;
+ LinkFollowBox.destinationDoc = dest;
+ this.fetchDocuments();
+
+ SelectionManager.SelectedDocuments().forEach(dv => {
+ if (dv.props.Document === LinkFollowBox.sourceDoc) {
+ this.sourceView = dv;
+ }
+ });
+
+ this.resetPan();
+ }
+
+ highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc);
+
+ @undoBatch
+ openFullScreen = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+ view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+ }
+ }
+
+ @undoBatch
+ openColFullScreen = (options: { context: Doc }) => {
+ if (LinkFollowBox.destinationDoc) {
+ if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ let view = DocumentManager.Instance.getDocumentView(options.context);
+ view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+ this.highlightDoc();
+ }
+ }
+
+ // should container be a doc or documentview or what? This one needs work and is more long term
+ @undoBatch
+ openInContainer = (options: { container: Doc }) => {
+
+ }
+
+ static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
+
+ static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
+ LinkFollowBox._addDocTab = addFunc;
+ }
+
+ @undoBatch
+ openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => {
+ if (LinkFollowBox.destinationDoc) {
+ options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
+ if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
+
+ if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkRight = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+
+ }
+
+ @undoBatch
+ jumpToLink = async (options: { shouldZoom: boolean }) => {
+ if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
+ 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));
+
+ DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
+ }
+ }
+
+ @undoBatch
+ openLinkTab = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ // this.prosp.addDocTab is empty -- use the link source's addDocTab
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => {
+ if (LinkFollowBox.destinationDoc) {
+ options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
+ if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
+ if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkInPlace = (options: { shouldZoom: boolean }) => {
+
+ if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
+ if (this.sourceView && this.sourceView.props.addDocument) {
+ 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);
+
+ alias.x = x + width + 30;
+ alias.y = y;
+ alias.width = width;
+ alias.height = height;
+
+ this.sourceView.props.addDocument(alias, false);
+ }
+ }
+
+ this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ //set this to be the default link behavior, can be any of the above
+ public defaultLinkBehavior: (options?: any) => void = this.jumpToLink;
+
+ @action
+ currentLinkBehavior = () => {
+ // this.resetPan();
+ if (LinkFollowBox.destinationDoc) {
+ if (this.selectedContextString === "") {
+ this.selectedContextString = "self";
+ this.selectedContext = LinkFollowBox.destinationDoc;
+ }
+ if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
+ let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
+ let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
+
+ if (this.selectedMode === FollowModes.INPLACE) {
+ if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.OPENFULL) {
+ if (notOpenInContext) this.openFullScreen();
+ else this.selectedContext && this.openColFullScreen({ context: this.selectedContext });
+ }
+ else if (this.selectedMode === FollowModes.OPENRIGHT) {
+ if (notOpenInContext) this.openLinkRight();
+ else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.OPENTAB) {
+ if (notOpenInContext) this.openLinkTab();
+ else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.PAN) {
+ this.jumpToLink({ shouldZoom: shouldZoom });
+ }
+ else return;
+ }
+ }
+
+ @action
+ handleModeChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedMode = target.value;
+ this.selectedContext = undefined;
+ this.selectedContextString = "";
+
+ this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN);
+
+ if (this.shouldUseOnlyParentContext) {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+ this.selectedContext = this.sourceView.props.ContainingCollectionDoc;
+ this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title));
+ }
+ }
+ }
+
+ @action
+ handleOptionChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedOption = target.value;
+ }
+
+ @action
+ handleContextChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedContextString = target.value;
+ // selectedContext is updated in reaction
+ this.selectedOption = "";
+ }
+
+ @computed
+ get canOpenInPlace() {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+ let colDoc = this.sourceView.props.ContainingCollectionDoc;
+ if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
+ }
+ return false;
+ }
+
+ @computed
+ get availableModes() {
+ return (
+ <div>
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENRIGHT}
+ checked={this.selectedMode === FollowModes.OPENRIGHT}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENRIGHT}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENTAB}
+ checked={this.selectedMode === FollowModes.OPENTAB}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENTAB}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENFULL}
+ checked={this.selectedMode === FollowModes.OPENFULL}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENFULL}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.PAN}
+ checked={this.selectedMode === FollowModes.PAN}
+ onChange={this.handleModeChange}
+ disabled={!this.canPan} />
+ {FollowModes.PAN}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.INPLACE}
+ checked={this.selectedMode === FollowModes.INPLACE}
+ onChange={this.handleModeChange}
+ disabled={!this.canOpenInPlace} />
+ {FollowModes.INPLACE}
+ </label><br />
+ </div>
+ );
+ }
+
+ @computed
+ get parentName() {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+ return this.sourceView.props.ContainingCollectionDoc.title;
+ }
+ }
+
+ @computed
+ get parentID(): string {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+ return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]);
+ }
+ return "col";
+ }
+
+ @computed
+ get availableContexts() {
+ return (
+ this.shouldUseOnlyParentContext ?
+ <label><input
+ type="radio" disabled={true}
+ name="context"
+ value={this.parentID}
+ checked={true} />
+ {this.parentName} (Parent Collection)
+ </label>
+ :
+ <div>
+ <label><input
+ type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
+ name="context"
+ value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"}
+ checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true}
+ onChange={this.handleContextChange} />
+ Open Self
+ </label><br />
+ {[...this._docs, ...this._otherDocs].map(doc => {
+ if (doc && doc.target && doc.col.title !== "Recently Closed") {
+ return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}>
+ <input
+ type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
+ name="context"
+ value={StrCast(doc.col[Id])}
+ checked={this.selectedContextString === StrCast(doc.col[Id])}
+ onChange={this.handleContextChange} />
+ {doc.col.title}
+ </label><br /></div>;
+ }
+ })}
+ </div>
+ );
+ }
+
+ @computed
+ get shouldShowZoom(): boolean {
+ if (this.selectedMode === FollowModes.OPENFULL) return false;
+ if (this.shouldUseOnlyParentContext) return true;
+ if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false;
+
+ let contextMatch: boolean = false;
+ if (this.selectedContextAliases) {
+ this.selectedContextAliases.forEach(alias => {
+ if (alias.viewType === CollectionViewType.Freeform) contextMatch = true;
+ });
+ }
+ if (contextMatch) return true;
+
+ return false;
+ }
+
+ @computed
+ get availableOptions() {
+ if (LinkFollowBox.destinationDoc) {
+ return (
+ this.shouldShowZoom ?
+ <div>
+ <label><input
+ type="radio"
+ name="option"
+ value={FollowOptions.ZOOM}
+ checked={this.selectedOption === FollowOptions.ZOOM}
+ onChange={this.handleOptionChange}
+ disabled={false} />
+ {FollowOptions.ZOOM}
+ </label><br />
+ <label><input
+ type="radio"
+ name="option"
+ value={FollowOptions.NOZOOM}
+ checked={this.selectedOption === FollowOptions.NOZOOM}
+ onChange={this.handleOptionChange}
+ disabled={false} />
+ {FollowOptions.NOZOOM}
+ </label><br />
+ </div>
+ :
+ <div>No Available Options</div>
+ );
+ }
+ return null;
+ }
+
+ render() {
+ return (
+ <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
+ <div className="linkFollowBox-header">
+ <div className="topHeader">
+ {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
+ <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
+ </div>
+ <div className=" direction-indicator">{LinkFollowBox.linkDoc ?
+ LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
+ : "" : ""}</div>
+ </div>
+ <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Mode</div>
+ <div className="linkFollowBox-itemContent">
+ {LinkFollowBox.linkDoc ? this.availableModes : "Please select a link to view modes"}
+ </div>
+ </div>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Context</div>
+ <div className="linkFollowBox-itemContent">
+ {this.selectedMode !== "" ? this.availableContexts : "Please select a mode to view contexts"}
+ </div>
+ </div>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Options</div>
+ <div className="linkFollowBox-itemContent">
+ {this.selectedContextString !== "" ? this.availableOptions : "Please select a context to view options"}
+ </div>
+ </div>
+ </div>
+ <div className="linkFollowBox-footer">
+ <button
+ onClick={this.resetVars}>
+ Clear Link
+ </button>
+ <div style={{ width: 20 }}></div>
+ <button
+ onClick={this.currentLinkBehavior}
+ disabled={(LinkFollowBox.linkDoc) ? false : true}>
+ Follow Link
+ </button>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index a4018bd2d..a4018bd2d 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 1908889e9..27af873b5 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,6 +1,6 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
+import { DocumentView } from "../nodes/DocumentView";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
@@ -16,7 +16,7 @@ library.add(faTrash);
interface Props {
docView: DocumentView;
changeFlyout: () => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
}
@observer
@@ -39,6 +39,7 @@ export class LinkMenu extends React.Component<Props> {
linkItems.push(
<LinkMenuGroup
key={groupType}
+ docView={this.props.docView}
sourceDoc={this.props.docView.props.Document}
group={group}
groupType={groupType}
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index e04044266..1891919ce 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,27 +1,27 @@
-import { action, observable } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action } from "mobx";
import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
-import { LinkMenuItem } from "./LinkMenuItem";
-import { LinkEditor } from "./LinkEditor";
-import './LinkMenu.scss';
-import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { emptyFunction } from "../../../Utils";
import { Docs } from "../../documents/Documents";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { DragManager, SetupDrag } from "../../util/DragManager";
+import { LinkManager } from "../../util/LinkManager";
import { UndoManager } from "../../util/UndoManager";
-import { StrCast } from "../../../new_fields/Types";
-import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
+import { DocumentView } from "../nodes/DocumentView";
+import './LinkMenu.scss';
+import { LinkMenuItem } from "./LinkMenuItem";
+import React = require("react");
interface LinkMenuGroupProps {
sourceDoc: Doc;
group: Doc[];
groupType: string;
showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ docView: DocumentView;
+
}
@observer
@@ -55,7 +55,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
if (opp) return opp;
}) as Doc[];
- let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
+ let dragData = new DragManager.DocumentDragData(draggedDocs);
DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
handlers: {
@@ -83,9 +83,13 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
let groupItems = this.props.group.map(linkDoc => {
let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
if (destination && this.props.sourceDoc) {
- return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
+ return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]}
+ groupType={this.props.groupType}
addDocTab={this.props.addDocTab}
- linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+ linkDoc={linkDoc}
+ sourceDoc={this.props.sourceDoc}
+ destinationDoc={destination}
+ showEditor={this.props.showEditor} />;
}
});
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 90b335933..a6ee9c2c6 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,18 +1,17 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
+import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
+import { Doc } from '../../../new_fields/Doc';
+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");
-import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
-import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types';
-import { observable, action } from 'mobx';
-import { LinkManager } from '../../util/LinkManager';
-import { DragLinkAsDocument } from '../../util/DragManager';
-import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { SelectionManager } from '../../util/SelectionManager';
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
@@ -22,7 +21,7 @@ interface LinkMenuItemProps {
sourceDoc: Doc;
destinationDoc: Doc;
showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
}
@observer
@@ -31,43 +30,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
@observable private _showMore: boolean = false;
@action toggleShowMore() { this._showMore = !this._showMore; }
- @undoBatch
- onFollowLink = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- e.persist();
- let jumpToDoc = this.props.destinationDoc;
- let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
- if (pdfDoc) {
- jumpToDoc = pdfDoc;
- }
- let proto = Doc.GetProto(this.props.linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let sourceContext = await Cast(proto.sourceContext, Doc);
- let self = this;
-
-
- let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
- if (e.ctrlKey) {
- dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined);
- }
-
- if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext);
- }
- else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!));
- }
- else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
- }
- else {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc);
- }
- }
-
onEdit = (e: React.PointerEvent): void => {
e.stopPropagation();
this.props.showEditor(this.props.linkDoc);
+ //SelectionManager.DeselectAll();
}
renderMetadata = (): JSX.Element => {
@@ -100,6 +66,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
onLinkButtonUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onLinkButtonMoved);
document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+ if (LinkFollowBox.Instance !== undefined) {
+ LinkFollowBox.Instance.props.Document.isMinimized = false;
+ LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+ LinkFollowBox.setAddDocTab(this.props.addDocTab);
+ }
e.stopPropagation();
}
@@ -113,6 +85,30 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
e.stopPropagation();
}
+ onContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ ContextMenu.Instance.addItem({ description: "Open in Link Follower", event: () => this.openLinkFollower(), icon: "link" });
+ ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ @action.bound
+ async followDefault() {
+ if (LinkFollowBox.Instance !== undefined) {
+ LinkFollowBox.setAddDocTab(this.props.addDocTab);
+ LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+ LinkFollowBox.Instance.defaultLinkBehavior();
+ }
+ }
+
+ @action.bound
+ async openLinkFollower() {
+ if (LinkFollowBox.Instance !== undefined) {
+ LinkFollowBox.Instance.props.Document.isMinimized = false;
+ LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+ }
+ }
+
render() {
let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
@@ -127,7 +123,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
{canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
<FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
<div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
- <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+ <div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
</div>
</div>
{this._showMore ? this.renderMetadata() : <></>}
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/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 54848344b..f08ea4891 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,7 +3,7 @@ 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 } from '../../../new_fields/Doc';
+import { Doc, DocListCastAsync, 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';
@@ -13,6 +13,8 @@ import { undoBatch } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
import './ButtonBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
library.add(faEdit as any);
@@ -41,11 +43,24 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
}
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({
+ description: "Clear Script Params", event: () => {
+ let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ params && params.map(p => this.props.Document[p] = undefined);
+ }, icon: "trash"
+ });
+
+ ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" });
+ }
+
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- if (de.data instanceof DragManager.DocumentDragData) {
- Doc.GetProto(this.dataDoc).source = new List<Doc>(de.data.droppedDocuments);
+ if (de.data instanceof DragManager.DocumentDragData && e.target) {
+ this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments);
e.stopPropagation();
}
}
@@ -53,9 +68,9 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
render() {
let params = Cast(this.props.Document.buttonParams, listSpec("string"));
let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
- params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ...
+ params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ...
return (
- <div className="buttonBox-outerDiv" ref={this.createDropTarget} >
+ <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-mainButtonCenter">
{(this.Document.text || this.Document.title)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
new file mode 100644
index 000000000..c0d9e1267
--- /dev/null
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -0,0 +1,5 @@
+.collectionFreeFormDocumentView-container {
+ transform-origin: left top;
+ position: absolute;
+ background-color: transparent;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7631ecc6c..93f6dc468 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,71 +1,88 @@
-import { computed } from "mobx";
+import { random } from "animejs";
+import { computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { percent2frac } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
-import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
-import "./DocumentView.scss";
+import "./CollectionFreeFormDocumentView.scss";
+import { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { Doc } from "../../../new_fields/Doc";
-import { returnEmptyString } from "../../../Utils";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
x?: number;
y?: number;
width?: number;
height?: number;
+ jitterRotation: number;
+ transition?: string;
}
-
-const schema = createSchema({
- zoomBasis: "number",
+export const positionSchema = createSchema({
zIndex: "number",
+ x: "number",
+ y: "number",
+ z: "number",
});
-//TODO Types: The import order is wrong, so positionSchema is undefined
-type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>;
-const FreeformDocument = makeInterface(schema, positionSchema);
+export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+export const PositionDocument = makeInterface(documentSchema, positionSchema);
@observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
- @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
- @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
- @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
- @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
- @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
- @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
- @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
- @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); }
+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](); }
+ @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 renderScriptDim() {
+ if (this.Document.renderScript) {
+ let someView = Cast(this.props.Document.someView, Doc);
+ let minimap = Cast(this.props.Document.minimap, Doc);
+ if (someView instanceof Doc && minimap instanceof Doc) {
+ let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
+ let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
+ let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+ let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+ return { x: x, y: y, width: w, height: h };
+ }
+ }
+ return undefined;
+ }
+
+ componentWillUnmount() {
+ this._disposer && this._disposer();
+ }
+ componentDidMount() {
+ 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]);
+ }, { fireImmediately: true });
+ }
- contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1;
+ contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth)
-
- animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
- this.props.bringToFront(this.props.Document);
- let targetPos = [this.Document.x || 0, this.Document.y || 0];
- let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]);
- DocumentView.animateBetweenIconFunc(this.props.Document,
- this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => {
- let pval = maximizing ?
- [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] :
- [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress];
- this.Document.x = progress === 1 ? targetPos[0] : pval[0];
- this.Document.y = progress === 1 ? targetPos[1] : pval[1];
- });
- }
+ .scale(1 / this.contentScaling())
borderRounding = () => {
- let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.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 br = StrCast((ld || this.props.Document).borderRounding);
+ br = !br && ruleRounding ? ruleRounding : br;
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.props.PanelWidth(), this.props.PanelHeight()));
- return minDim;
+ 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;
}
@@ -75,21 +92,30 @@ 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() {
- const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
return (
<div className="collectionFreeFormDocumentView-container"
style={{
- transformOrigin: "left top",
- position: "absolute",
- backgroundColor: "transparent",
- boxShadow: this.props.Document.opacity === 0 ? undefined : this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
- this.clusterColor ? (
- this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :
- `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined,
+ 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: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),
+ 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,
@@ -98,9 +124,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
backgroundColor={this.clusterColorFunc}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- animateBetweenIcon={this.animateBetweenIcon}
+ PanelWidth={this.finalPanelWidth}
+ PanelHeight={this.finalPanelHeight}
/>
</div>
);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d77662355..75dd27f46 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,35 +1,35 @@
-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 } 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 { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
import { DragBox } from "./DragBox";
-import { ButtonBox } from "./ButtonBox";
-import { PresBox } from "./PresBox";
+import { FieldView, FieldViewProps } from "./FieldView";
+import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
+import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
+import { PresBox } from "./PresBox";
+import { 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";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -91,13 +91,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
return { props: list };
}
- @computed get templates(): List<string> {
- let field = this.props.Document.templates;
- if (field && field instanceof List) {
- return field;
- }
- return new List<string>();
- }
@computed get finalLayout() {
return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
}
@@ -105,10 +98,10 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
render() {
let self = this;
if (this.props.renderDepth > 7) return (null);
- if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) 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 }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7c72fb6e6..b3e7898c1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,6 +4,9 @@
position: inherit;
top: 0;
left:0;
+ border-radius: inherit;
+ transition : outline .3s linear;
+
// background: $light-color; //overflow: hidden;
transform-origin: left top;
@@ -27,7 +30,62 @@
overflow-y: scroll;
height: calc(100% - 20px);
}
+
+ .documentView-overlays {
+ border-radius: inherit;
+ position: absolute;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ .documentView-textOverlay {
+ border-radius: inherit;
+ width: 100%;
+ display: inline-block;
+ position: absolute;
+ }
+ }
}
.documentView-node-topmost {
background: white;
+}
+
+.documentView-styleWrapper {
+ position: absolute;
+ display: inline-block;
+ width:100%;
+ height:100%;
+ pointer-events: none;
+
+ .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 7cbd96354..7dbefe087 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,17 +1,14 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx";
+import { action, computed, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { Copy, Id } from '../../../new_fields/FieldSymbols';
-import { List } from "../../../new_fields/List";
-import { ObjectField } from "../../../new_fields/ObjectField";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Id } from '../../../new_fields/FieldSymbols';
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { RouteStore } from '../../../server/RouteStore';
import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
@@ -35,14 +32,17 @@ import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
-import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
import { DocumentType } from '../../documents/DocumentTypes';
-const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { ImageField } from '../../../new_fields/URLField';
+import SharingManager from '../../util/SharingManager';
+import { Scripting } from '../../util/Scripting';
+library.add(fa.faEdit);
library.add(fa.faTrash);
library.add(fa.faShare);
library.add(fa.faDownload);
@@ -65,19 +65,9 @@ library.add(fa.faUnlock);
library.add(fa.faLock);
library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
-// const linkSchema = createSchema({
-// title: "string",
-// linkDescription: "string",
-// linkTags: "string",
-// linkedTo: Doc,
-// linkedFrom: Doc
-// });
-
-// type LinkDoc = makeInterface<[typeof linkSchema]>;
-// const LinkDoc = makeInterface(linkSchema);
-
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionDoc: Opt<Doc>;
Document: Doc;
DataDoc?: Doc;
fitToBox?: boolean;
@@ -89,48 +79,52 @@ export interface DocumentViewProps {
renderDepth: number;
showOverlays?: (doc: Doc) => { title?: string, caption?: string };
ContentScaling: () => number;
+ ruleProvider: Doc | undefined;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Doc, willZoom: boolean, scale?: number) => void;
- selectOnLoad: boolean;
+ focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
- collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
zoomToScale: (scale: number) => void;
backgroundColor: (doc: Doc) => string | undefined;
getScale: () => number;
- animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
+ animateBetweenIcon?: (maximize: boolean, target: number[]) => void;
ChromeHeight?: () => number;
}
-const schema = createSchema({
- layout: "string",
- nativeWidth: "number",
- nativeHeight: "number",
- backgroundColor: "string",
- opacity: "number",
- hidden: "boolean",
- onClick: ScriptField,
-});
-
-export const positionSchema = createSchema({
- nativeWidth: "number",
- nativeHeight: "number",
- width: "number",
- height: "number",
- x: "number",
- y: "number",
- z: "number",
+export const documentSchema = createSchema({
+ // 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
+ 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)
});
-export type PositionDocument = makeInterface<[typeof positionSchema]>;
-export const PositionDocument = makeInterface(positionSchema);
-type Document = makeInterface<[typeof schema]>;
-const Document = makeInterface(schema);
+type Document = makeInterface<[typeof documentSchema]>;
+const Document = makeInterface(documentSchema);
@observer
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -138,115 +132,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _downY: number = 0;
private _lastTap: number = 0;
private _doubleTap = false;
- private _hitExpander = false;
private _hitTemplateDrag = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
- _animateToIconDisposer?: IReactionDisposer;
- _reactionDisposer?: IReactionDisposer;
public get ContentDiv() { return this._mainCont.current; }
- @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
- @computed get topMost(): boolean { return this.props.renderDepth === 0; }
- @computed get templates(): List<string> {
- let field = this.props.Document.templates;
- if (field && field instanceof List) {
- return field;
- }
- return new List<string>();
- }
- set templates(templates: List<string>) { this.props.Document.templates = templates; }
- screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ @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 onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
@action
componentDidMount() {
- if (this._mainCont.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
- handlers: { drop: this.drop.bind(this) }
- });
- }
- // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
- this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title),
- this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
- () => {
- let maxDoc = DocListCast(this.props.Document.maximizedDocs);
- if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
- this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon";
- }
- let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
- if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) {
- this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
- }
- }, { fireImmediately: true });
- this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
- (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
- , { fireImmediately: true });
+ this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }));
DocumentManager.Instance.DocumentViews.push(this);
}
- animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
- this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) :
- DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing);
- }
-
- public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => {
- setTimeout(() => {
- let now = Date.now();
- let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1;
- doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- cb && cb(progress);
- if (now < stime + 200) {
- DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);
- }
- else {
- doc.isMinimized = !maximizing;
- doc.isIconAnimating = undefined;
- }
- doc.willMaximize = false;
- },
- 2);
- }
@action
componentDidUpdate() {
this._dropDisposer && this._dropDisposer();
- if (this._mainCont.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
- handlers: { drop: this.drop.bind(this) }
- });
- }
+ this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }));
}
+
@action
componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- this._animateToIconDisposer && this._animateToIconDisposer();
this._dropDisposer && this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
- stopPropagation = (e: React.SyntheticEvent) => {
- e.stopPropagation();
- }
-
- 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 temlplate 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.
- return this.props.Document;
- }
- return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
- }
- startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) {
if (this._mainCont.current) {
- let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
- let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+ let dragData = new DragManager.DocumentDragData([this.props.Document]);
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected);
- const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
+ dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
- dragData.xOffset = xoff;
- dragData.yOffset = yoff;
dragData.moveDocument = this.props.moveDocument;
dragData.applyAsTemplate = applyAsTemplate;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
@@ -257,163 +177,92 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
}
- toggleMinimized = async () => {
- let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
- if (minimizedDoc) {
- let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(
- NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
- this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
- }
- }
- static _undoBatch?: UndoManager.Batch = undefined;
- @action
- public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- SelectionManager.DeselectAll();
- if (expandedDocs) {
- if (!DocumentView._undoBatch) {
- DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
- }
- let isMinimized: boolean | undefined;
- expandedDocs.map(maximizedDoc => {
- let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
- if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
- if (isMinimized === undefined) {
- isMinimized = BoolCast(maximizedDoc.isMinimized);
- }
- maximizedDoc.willMaximize = isMinimized;
- maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
+ onClick = async (e: React.MouseEvent) => {
+ 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();
+ let preventDefault = true;
+ if (this._doubleTap && this.props.renderDepth) {
+ 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");
}
- });
- setTimeout(() => {
- DocumentView._undoBatch && DocumentView._undoBatch.end();
- DocumentView._undoBatch = undefined;
- }, 500);
+ 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);
+ } 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);
+ preventDefault = false;
+ }
+ preventDefault && e.preventDefault();
}
}
- onClick = async (e: React.MouseEvent) => {
- if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
- if (this.onClickHandler && this.onClickHandler.script) {
- e.stopPropagation();
- this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document });
- e.preventDefault();
- return;
- }
- let altKey = e.altKey;
- let ctrlKey = e.ctrlKey;
- if (this._doubleTap && this.props.renderDepth) {
- e.stopPropagation();
- let fullScreenAlias = Doc.MakeAlias(this.props.Document);
- fullScreenAlias.templates = new List<string>();
- Doc.UseDetailLayout(fullScreenAlias);
- fullScreenAlias.showCaption = true;
- this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
+ buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
+ let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+ let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+ let expandedDocs: Doc[] = [];
+ expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+ // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
+ if (expandedDocs.length) {
SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
- }
- else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
- (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
- if (BoolCast(this.props.Document.ignoreClick)) {
- return;
- }
- e.stopPropagation();
- SelectionManager.SelectDoc(this, e.ctrlKey);
- let isExpander = (e.target as any).id === "isExpander";
- if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) {
- let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
- 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 expandedDocs: Doc[] = [];
- expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
- expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
- expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
- // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
- if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
- SelectionManager.DeselectAll();
- let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
- let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
- if (altKey || ctrlKey) {
- maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"));
- if (!maxLocation || maxLocation === "inPlace") {
- let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
- let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false);
- expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
- let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
- if (!hasView) {
- this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
- }
- expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
- }
- }
- if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
- let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
- if (dataDocs) {
- expandedDocs.forEach(maxDoc =>
- (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
- this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation)));
- }
- } else {
- let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
- this.collapseTargetsToPoint(scrpt, expandedDocs);
- }
- }
- else if (linkedDocs.length) {
- SelectionManager.DeselectAll();
- let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
- let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
-
- // @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.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
- let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
- document => { // open up target if it's not already in view ...
- this.props.focus(this.props.Document, true, 1); // by zooming into the button document first
- setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab
- },
- linkedFwdPage[altKey ? 1 : 0], targetContext);
- }
- }
+ 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);
+ DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
+ } else {
+ expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
}
}
+ else if (linkDocs.length) {
+ 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._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
this._hitTemplateDrag = false;
+ // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where
+ // this document is the template and we apply it to whatever we drop it on.
for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
this._hitTemplateDrag = true;
}
}
- if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
+ if (this.active && 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);
+ document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && this.active) {
- if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
- if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !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) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, 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
@@ -431,71 +280,79 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
@undoBatch
- fieldsClicked = (): void => {
- let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 });
- this.props.addDocTab(kvp, this.dataDoc, "onRight");
- }
-
- @undoBatch
- makeBtnClicked = (): void => {
- let doc = Doc.GetProto(this.props.Document);
- doc.isButton = !BoolCast(doc.isButton);
- if (doc.isButton) {
- if (!doc.nativeWidth) {
- doc.nativeWidth = this.props.Document[WidthSym]();
- doc.nativeHeight = this.props.Document[HeightSym]();
+ static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom")
+
+ static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => {
+ const batch = UndoManager.StartBatch("CustomViewClicked");
+ if (doc.layoutCustom === undefined) {
+ Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative");
+ await swapViews(doc, "", "layoutNative");
+
+ 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;
+ default:
+ fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
}
+
+ fieldTemplate.backgroundColor = doc.backgroundColor;
+ fieldTemplate.heading = 1;
+ fieldTemplate.autoHeight = true;
+
+ 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, doc, undefined);
+ Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom");
} else {
- doc.nativeWidth = doc.nativeHeight = undefined;
+ await swapViews(doc, "layoutCustom", "layoutNative");
}
+ batch.end();
}
@undoBatch
- public fullScreenClicked = (): void => {
- CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this);
- SelectionManager.DeselectAll();
+ makeBtnClicked = (): void => {
+ if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
+ this.Document.isButton = false;
+ this.Document.ignoreClick = false;
+ this.Document.onClick = undefined;
+ } else {
+ this.Document.isButton = true;
+ }
}
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
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 annotationDoc = de.data.annotationDocument;
- annotationDoc.linkedToDoc = true;
- de.data.targetContext = this.props.ContainingCollectionView!.props.Document;
- let targetDoc = this.props.Document;
- targetDoc.targetContext = de.data.targetContext;
- let annotations = await DocListCastAsync(annotationDoc.annotations);
- annotations && annotations.forEach(anno => anno.target = targetDoc);
-
- DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.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, this.props.DataDoc);
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document);
e.stopPropagation();
}
if (de.data instanceof DragManager.LinkDragData) {
- let sourceDoc = de.data.linkSourceDocument;
- let destDoc = this.props.Document;
-
e.stopPropagation();
- if (de.mods === "AltKey") {
- const protoDest = destDoc.proto;
- const protoSrc = sourceDoc.proto;
- let src = protoSrc ? protoSrc : sourceDoc;
- let dst = protoDest ? protoDest : destDoc;
- dst.data = (src.data! as ObjectField)[Copy]();
- dst.nativeWidth = src.nativeWidth;
- dst.nativeHeight = src.nativeHeight;
- }
- else {
- // 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);
- let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
- de.data.droppedDocuments.push(destDoc);
- de.data.linkDocument = linkDoc;
- }
+ // 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({ 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
}
}
@@ -503,58 +360,61 @@ 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 = FieldValue(this.Document.layout) || "";
+ let oldLayout = StrCast(this.props.Document.layout);
let layout = text.replace("{layout}", oldLayout);
- this.Document.layout = layout;
+ this.props.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
}
+ @undoBatch
@action
- addTemplate = (template: Template) => {
- this.templates.push(template.Layout);
- this.templates = this.templates;
+ 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();
+ }
}
+ @undoBatch
@action
- removeTemplate = (template: Template) => {
- for (let i = 0; i < this.templates.length; i++) {
- if (this.templates[i] === template.Layout) {
- this.templates.splice(i, 1);
- break;
- }
+ makeIntoPortal = async () => {
+ let anchors = await Promise.all(DocListCast(this.props.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({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
+ this.Document.isButton = true;
+ });
}
- this.templates = this.templates;
- }
- @action
- clearTemplates = () => {
- this.templates.length = 0;
- this.templates = this.templates;
}
@undoBatch
@action
- freezeNativeDimensions = (): void => {
- let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
- this.props.Document.autoHeight = proto.autoHeight = false;
- proto.ignoreAspect = !BoolCast(proto.ignoreAspect);
- if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) {
- proto.nativeWidth = this.props.PanelWidth();
- proto.nativeHeight = this.props.PanelHeight();
+ setCustomView = (custom: boolean): void => {
+ 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 ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
}
}
+
@undoBatch
@action
makeBackground = (): void => {
- this.props.Document.isBackground = !this.props.Document.isBackground;
- this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true);
+ this.Document.isBackground = !this.Document.isBackground;
+ this.Document.isBackground && this.props.bringToFront(this.Document, true);
}
@undoBatch
@action
toggleLockPosition = (): void => {
- this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
+ this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true;
}
listen = async () => {
@@ -582,45 +442,60 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const cm = ContextMenu.Instance;
let subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
- subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" });
- subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" });
- subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
+ subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" });
+ subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
- let existingMake = ContextMenu.Instance.findByDescription("Make...");
- let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
- makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" });
- makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" });
- makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") });
- makes.push({
- description: "Into Portal", event: () => {
- let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
- DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal");
- this.makeBtnClicked();
- }, icon: "window-restore"
+
+ 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" });
+ onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" });
+ onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+ onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
+ onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
+ onClicks.push({
+ description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => {
+ this.props.Document.collectionContext = this.props.ContainingCollectionDoc;
+ ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n");
+ }
});
- makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" });
- !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
+ !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+
let existing = ContextMenu.Instance.findByDescription("Layout...");
let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
-
- layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" });
- layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
- layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
+ layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
+ if (this.props.DataDoc) {
+ 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.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.props.Document.detailedLayout && !this.props.Document.isTemplate) {
- layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
+ if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
+ layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+ } else if (this.props.Document.layoutNative) {
+ 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 : [];
@@ -629,44 +504,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle!
cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
cm.addItem({
- description: "Download document", icon: "download", event: () => {
- const a = document.createElement("a");
- const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
- a.href = url;
- a.download = `DocExport-${this.props.Document[Id]}.zip`;
- a.click();
- }
+ description: "Download document", icon: "download", event: async () =>
+ console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
+ qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' }
+ })))
+ // const a = document.createElement("a");
+ // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+ // a.href = url;
+ // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+ // a.click();
});
+ 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" });
- type User = { email: string, userDocumentId: string };
- let usersMenu: ContextMenuProps[] = [];
- try {
- let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
- const users: User[] = JSON.parse(stuff);
- usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({
- description: email, event: async () => {
- const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
- if (!userDocument) {
- throw new Error(`Couldn't get user document of user ${email}`);
- }
- const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
- if (notifDoc instanceof Doc) {
- 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"
- }));
- } catch {
-
- }
runInAction(() => {
- cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
if (!ClientUtils.RELEASE) {
let setWriteMode = (mode: DocServer.WriteMode) => {
DocServer.AclsMode = mode;
@@ -690,6 +541,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
@@ -702,117 +560,181 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
- onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
- onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
+
+ // the document containing the view layout information - will be the Document itself unless the Document has
+ // a layout field. In that case, all layout information comes from there unless overriden by Document
+ get layoutDoc(): Document {
+ return Document(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.
+ getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
+ getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
isSelected = () => SelectionManager.IsSelected(this);
- @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
- @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+ select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
+
+ chromeHeight = () => {
+ let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
+ let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle);
+ return (showTitle ? 25 : 0) + 1;
+ }
+
+ childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
@computed get contents() {
- return (<DocumentContentsView {...this.props}
+ 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}
- selectOnLoad={this.props.selectOnLoad}
- layoutKey={"layout"}
- fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
- DataDoc={this.dataDoc} />);
+ layoutKey="layout"
+ DataDoc={this.props.DataDoc} />);
}
-
- chromeHeight = () => {
- let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
- let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- let templates = Cast(this.layoutDoc.templates, listSpec("string"));
- if (!showOverlays && templates instanceof List) {
- templates.map(str => {
- if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
- });
- }
- return (showTitle ? 25 : 0) + 1;// bcz: why 8??
- }
-
- 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;
- }
-
-
render() {
- let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ?
- this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) :
- StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc);
- let foregroundColor = StrCast(this.layoutDoc.color);
- var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%";
- var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
- let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
- let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption);
- let templates = Cast(this.layoutDoc.templates, listSpec("string"));
- if (!showOverlays && templates instanceof List) {
- templates.map(str => {
- if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
- if (!showCaption && str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";
- });
- }
- let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
- let brushDegree = Doc.IsBrushedDegree(this.props.Document);
- let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding);
- let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree;
+ let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : 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);
+
+ const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+ const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ 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}
+ </div>);
+ const captionView = (!showCaption ? (null) :
+ <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+ <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}
+ />
+ </div>);
+ const titleView = (!showTitle ? (null) :
+ <div className="documentView-titleWrapper" style={{
+ 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]}
+ display={"block"} height={72} fontSize={12}
+ GetValue={() => StrCast(this.Document[showTitle])}
+ SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true}
+ />
+ </div>);
+ 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"];
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
- pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
- color: foregroundColor,
- outlineColor: ["transparent", "maroon", "maroon"][brushDegree],
- outlineStyle: ["none", "dashed", "solid"][brushDegree],
- outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px",
- border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined,
- borderRadius: "inherit",
+ 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),
+ outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+ border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
background: backgroundColor,
- width: nativeWidth,
- height: nativeHeight,
- transform: `scale(${this.props.ContentScaling()})`,
+ width: animwidth,
+ height: animheight,
+ transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`,
opacity: this.Document.opacity
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
>
- {!showTitle && !showCaption ? this.contents :
- <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
-
- <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
+ {!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>
- {!showTitle ? (null) :
- <div style={{
- position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
- pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
- overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",
- transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
- }}>
- <EditableView
- contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]}
- display={"block"}
- height={72}
- fontSize={12}
- GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])}
- SetValue={(value: string) => ((this.layoutDoc.isTemplate ? this.layoutDoc : Doc.GetProto(this.layoutDoc))[showTitle!] = value) ? true : true}
- />
- </div>
- }
- {!showCaption ? (null) :
- <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
- <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
- </div>
- }
+ {titleView}
+ {captionView}
+ {searchHighlight}
</div>
}
</div>
);
}
-} \ No newline at end of file
+}
+
+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;
+ }
+
+ 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;
+ }
+}
+
+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
diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx
index 1f2c88086..6c3db18c4 100644
--- a/src/client/views/nodes/DragBox.tsx
+++ b/src/client/views/nodes/DragBox.tsx
@@ -45,17 +45,15 @@ export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocu
}
onDragMove = (e: MouseEvent) => {
- if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
+ 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 }) : undefined;
- let doc = res !== undefined && res.success ?
- res.result as Doc :
- Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
- doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY);
+ 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();
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index f0f1b3b73..b93c78cfd 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,15 +30,16 @@ export interface FieldViewProps {
leaveNativeSize?: boolean;
fitToBox?: boolean;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionDoc: Opt<Doc>;
+ ruleProvider: Doc | undefined;
Document: Doc;
DataDoc?: Doc;
onClick?: ScriptField;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
- selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
@@ -49,7 +50,6 @@ export interface FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
- setPdfBox?: (player: PDFBox) => void;
ContentScaling: () => number;
ChromeHeight?: () => number;
}
@@ -95,7 +95,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- return <p><b>{field.title}</b></p>;
+ return <p><b>{field.title && field.title.toString()}</b></p>;
//return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
// let returnHundred = () => 100;
// return (
@@ -108,7 +108,6 @@ export class FieldView extends React.Component<FieldViewProps> {
// PanelWidth={returnHundred}
// PanelHeight={returnHundred}
// renderDepth={0} //TODO Why is renderDepth reset?
- // selectOnLoad={false}
// focus={emptyFunction}
// isSelected={this.props.isSelected}
// select={returnFalse}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 1b537cc52..29e8b14a8 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -4,7 +4,6 @@
width: 100%;
height: 100%;
min-height: 100%;
- font-family: $serif;
}
.ProseMirror:focus {
@@ -29,7 +28,7 @@
}
.formattedTextBox-cont-hidden {
- pointer-events: none;
+ // pointer-events: none;
}
.formattedTextBox-inner-rounded {
@@ -41,9 +40,10 @@
left: 10%;
}
-.formattedTextBox-inner-rounded div,
-.formattedTextBox-inner div {
+.formattedTextBox-inner-rounded ,
+.formattedTextBox-inner {
padding: 10px 10px;
+ height: 100%;
}
.menuicon {
@@ -65,4 +65,115 @@
.em {
font-style: italic;
-} \ No newline at end of file
+}
+
+.userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+}
+.userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width:10px;
+ min-height:10px;
+ text-align:center;
+ align-content: center;
+}
+
+footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ div {
+ padding : 0 !important;
+ }
+}
+
+footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+}
+
+.ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+
+.footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+}
+
+.prosemirror-attribution {
+ font-size: 8px;
+}
+.footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+}
+
+.formattedTextBox-summarizer {
+ opacity :0.5;
+ position: relative;
+ width:40px;
+ height:20px;
+}
+.formattedTextBox-summarizer::after{
+ content: "←" ;
+}
+
+.formattedTextBox-summarizer-collapsed {
+ opacity :0.5;
+ position: relative;
+ width:40px;
+ height:20px;
+}
+.formattedTextBox-summarizer-collapsed::after {
+ content: "...";
+}
+
+.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; }
+.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}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50}
+.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 0e04bacf7..9ca77cc8b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,49 +1,54 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import _ from "lodash";
import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
+import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection } 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 } 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 } 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 } 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 { 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 { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
+import { TextShadowProperty } from 'csstype';
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;
@@ -60,33 +65,39 @@ export const GoogleRef = "googleDocId";
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
-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);
}
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
- private _ref: React.RefObject<HTMLDivElement>;
+ private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
- private _linkClicked = "";
- private _reactionDisposer: Opt<IReactionDisposer>;
+ private _nodeClicked: any;
+ private _searchIndex = 0;
+ private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
+ private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
+ private _reactionDisposer: Opt<IReactionDisposer>;
private _textReactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
+ private _rulesReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
- private pullReactionDisposer: Opt<IReactionDisposer>;
- private pushReactionDisposer: Opt<IReactionDisposer>;
+ private _pullReactionDisposer: Opt<IReactionDisposer>;
+ private _pushReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @observable _entered = false;
- @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
- public static InputBoxOverlayScroll: number = 0;
+ @observable private _fontSize = 13;
+ @observable private _fontFamily = "Arial";
+ @observable private _fontAlign = "";
+ @observable private _entered = false;
+ public static SelectOnLoad = "";
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
}
@@ -108,172 +119,265 @@ 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
public setFontColor(color: string) {
- let self = this;
- if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
- if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
+ 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;
}
- let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color });
- this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from,
- this._editorView!.state.selection.to, colorMark));
+ 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) {
super(props);
FormattedTextBox.Instance = this;
- this._ref = React.createRef();
- if (this.props.isOverlay) {
- DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
- }
-
- document.addEventListener("paste", this.paste);
}
- @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @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 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); }
- paste = (e: ClipboardEvent) => {
- if (e.clipboardData && this._editorView) {
- // let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
- // for (let i = 0; i < e.clipboardData.items.length; i++) {
- // let item = e.clipboardData.items.item(i);
- // console.log(item)
- // if (item.type === "text/plain") {
- // console.log("plain")
- // item.getAsString((text) => {
- // let pdfPasteIndex = text.indexOf(pdfPasteText);
- // if (pdfPasteIndex > -1) {
- // let insertText = text.substr(0, pdfPasteIndex);
- // const tx = this._editorView!.state.tr.insertText(insertText);
- // // tx.setSelection(new Selection(tx.))
- // const state = this._editorView!.state;
- // this._editorView!.dispatch(tx);
- // if (FormattedTextBox._toolTipTextMenu) {
- // // this._toolTipTextMenu.makeLinkWithState(state)
- // }
- // e.stopPropagation();
- // e.preventDefault();
- // }
- // });
- // }
- // }
- }
+ linkOnDeselect: Map<string, string> = new Map();
+
+ doLinkOnDeselect() {
+ Array.from(this.linkOnDeselect.entries()).map(entry => {
+ let key = entry[0];
+ let value = entry[1];
+ let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+ DocServer.GetRefField(value).then(doc => {
+ DocServer.GetRefField(id).then(linkDoc => {
+ this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+ DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
+ if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
+ else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
+ });
+ });
+ });
+ this.linkOnDeselect.clear();
}
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
+ let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+ if (metadata) {
+ let range = tx.selection.$from.blockRange(tx.selection.$to);
+ let text = range ? tx.doc.textBetween(range.start, range.end) : "";
+ let textEndSelection = tx.selection.to;
+ for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { }
+ text = text.substr(0, textEndSelection - range!.start);
+ text = text.split(" ")[text.split(" ").length - 1];
+ let split = text.split("::");
+ if (split.length > 1 && split[1]) {
+ let key = split[0];
+ let value = split[split.length - 1];
+ 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();
+ 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;
+ }
+ }
const state = this._editorView.state.apply(tx);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
this._editorView.updateState(state);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
- if (state.selection.empty && FormattedTextBox._toolTipTextMenu) {
- const marks = tx.storedMarks;
- if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
+ if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
+ FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
this._applyingChange = true;
- const fieldkey = "preview";
- if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
- if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
+ 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())));
this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
- let title = StrCast(this.dataDoc.title);
- if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) {
- let str = this._editorView.state.doc.textContent;
- let titlestr = str.substr(0, Math.min(40, str.length));
- this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
- }
+ this.updateTitle();
+ this.tryUpdateHeight();
+ }
+ }
+
+ updateTitle = () => {
+ if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
}
- public highlightSearchTerms = (terms: String[]) => {
+ public highlightSearchTerms = (terms: string[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const fieldkey = "preview";
- 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 fieldkey = 'search_string';
- // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
- // this.props.Document[fieldkey] = undefined;
- // }
- // else this.props.Document.proto![fieldkey] = undefined;
- // }
+ 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)));
+ }
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
+ this.dropDisposer && this.dropDisposer();
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@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) {
+ if (de.data instanceof DragManager.EmbedDragData) {
+ 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;
- this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
+ const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+ let alias = Doc.MakeAlias(target);
+ alias.fitToBox = true;
+ let node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: alias[Id],
+ float: "right"
+ });
+ let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
+ link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
+ this.tryUpdateHeight();
e.stopPropagation();
- } else {
- if (de.data instanceof DragManager.DocumentDragData) {
- this.props.Document.layout = de.data.draggedDocuments[0];
- de.data.draggedDocuments[0].isTemplate = true;
+ } else 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 if (de.mods === "CtrlKey") {
+ 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;
+ }
+ }
e.stopPropagation();
}
}
}
- recordKeyHandler = (e: KeyboardEvent) => {
- if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) {
- return;
+ 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;
}
- 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("My Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" });
+ }
+ 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: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+ ["My Text", "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" });
}
recordBullet = async () => {
@@ -306,88 +410,78 @@ 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)));
+ }
}
}
private newListItems = (count: number) => {
- let listItems: any[] = [];
- for (let i = 0; i < count; i++) {
- listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
- }
- return listItems;
+ return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
}
- componentDidMount() {
- const config = {
+ _keymap: any = undefined;
+ @computed get config() {
+ this._keymap = buildKeymap(schema);
+ (schema as any).Document = this.props.Document;
+ return {
schema,
- inpRules, //these currently don't do anything, but could eventually be helpful
- plugins: this.props.isOverlay ? [
+ plugins: [
+ inputRules(inpRules),
this.tooltipTextMenuPlugin(),
history(),
- keymap(buildKeymap(schema)),
+ keymap(this._keymap),
keymap(baseKeymap),
// this.tooltipLinkingMenuPlugin(),
new Plugin({
props: {
attributes: { class: "ProseMirror-example-setup-style" }
}
- })
- ] : [
- history(),
- keymap(buildKeymap(schema)),
- keymap(baseKeymap),
- ]
+ }),
+ formattedTextBoxCommentPlugin
+ ]
};
+ }
- 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 });
- }
-
+ componentDidMount() {
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) {
let updatedState = JSON.parse(incomingValue);
- this._editorView.updateState(EditorState.fromJSON(config, updatedState));
+ this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateHeight();
}
}
);
- this.pullReactionDisposer = reaction(
+ this._pullReactionDisposer = reaction(
() => this.props.Document[Pulls],
() => {
- if (!DocumentDecorations.hasPulledHack) {
- DocumentDecorations.hasPulledHack = true;
+ if (!DocumentButtonBar.hasPulledHack) {
+ DocumentButtonBar.hasPulledHack = true;
let unchanged = this.dataDoc.unchanged;
this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
}
}
);
- this.pushReactionDisposer = reaction(
+ this._pushReactionDisposer = reaction(
() => this.props.Document[Pushes],
() => {
- if (!DocumentDecorations.hasPushedHack) {
- DocumentDecorations.hasPushedHack = true;
+ if (!DocumentButtonBar.hasPushedHack) {
+ DocumentButtonBar.hasPushedHack = true;
this.pushToGoogleDoc();
}
}
@@ -408,16 +502,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc.lastModified = undefined;
}
}, { fireImmediately: true });
- this.setupEditor(config, this.dataDoc, this.props.fieldKey);
+
+
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.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]);
}
@@ -425,31 +516,114 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.unhighlightSearchTerms();
}
}, { fireImmediately: true });
+
+
+ this._rulesReactionDisposer = reaction(() => {
+ let ruleProvider = this.props.ruleProvider;
+ let heading = NumCast(this.props.Document.heading);
+ if (ruleProvider instanceof Doc) {
+ return {
+ align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
+ font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"),
+ size: NumCast(ruleProvider["ruleSize_" + heading], 13)
+ };
+ }
+ return undefined;
+ },
+ action((rules: any) => {
+ this._fontFamily = rules ? rules.font : "Arial";
+ this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13);
+ rules && setTimeout(() => {
+ const view = this._editorView!;
+ if (this._proseRef) {
+ let n = new NodeSelection(view.state.doc.resolve(0));
+ if (this._editorView!.state.doc.textContent === "") {
+ view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))).
+ replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true));
+ } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) {
+ view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align }));
+ }
+ this.tryUpdateHeight();
+ }
+ }, 0);
+ }), { fireImmediately: true }
+ );
+ 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 && 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);
+ }
+ this.props.Document.scrollToLinkID = undefined;
+ }
+
+ },
+ { 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);
dataDoc.unchanged = pushSuccess;
- DocumentDecorations.Instance.startPushOutcome(pushSuccess);
+ DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
}
};
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 });
}
@@ -462,53 +636,44 @@ 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];
}
- DocumentDecorations.Instance.startPullOutcome(pullSuccess);
+ 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;
- DocumentDecorations.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);
}
}
-
clipboardTextSerializer = (slice: Slice): string => {
let text = "", separated = true;
const from = 0, to = slice.content.size;
@@ -532,65 +697,48 @@ 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.resolvedFieldDataDoc(doc, "data", "true");
- 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 () => {
+ let targetAnnotations = await DocListCastAsync(Doc.fieldExtensionDoc(pdfDoc, "data").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);
- 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) {
+ function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
const nodes: Node[] = [];
- frag.forEach(node => nodes.push(addLinkMark(node)));
+ frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
- function addLinkMark(node: Node) {
+ function addLinkMark(node: Node, title: string, linkId: string) {
if (!node.isText) {
- const content = addMarkToFrag(node.content);
+ const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
return node.copy(content);
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" });
- if (linkIndex !== -1) {
- marks.splice(linkIndex, 1, link);
- } else {
- marks.push(link);
- }
+ const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
+ marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
}
}
@@ -608,12 +756,26 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
if (this._proseRef) {
+ let self = this;
+ this._editorView && this._editorView.destroy();
this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
+ handleScrollToSelection: (editorView) => {
+ let ref = editorView.domAtPos(editorView.state.selection.from);
+ let refNode = ref.node as any;
+ while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement;
+ let r1 = refNode && refNode.getBoundingClientRect();
+ let r3 = self._ref.current!.getBoundingClientRect();
+ r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale);
+ return true;
+ },
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
+ 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(); },
+ footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
@@ -624,136 +786,166 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- if (this.props.selectOnLoad) {
- if (!this.props.isOverlay) this.props.select(false);
- else this._editorView!.focus();
+ let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
+ if (selectOnLoad) {
+ FormattedTextBox.SelectOnLoad = "";
+ this.props.select(false);
}
+ this._editorView!.focus();
+ // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
+ }
+ getFont(font: string) {
+ switch (font) {
+ case "Arial": return schema.marks.arial.create();
+ case "Times New Roman": return schema.marks.timesNewRoman.create();
+ case "Georgia": return schema.marks.georgia.create();
+ case "Comic Sans MS": return schema.marks.comicSans.create();
+ case "Tahoma": return schema.marks.tahoma.create();
+ case "Impact": return schema.marks.impact.create();
+ case "ACrimson Textrial": return schema.marks.crimson.create();
+ }
+ return schema.marks.arial.create();
}
componentWillUnmount() {
- this._editorView && this._editorView.destroy();
+ this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer();
+ this._rulesReactionDisposer && this._rulesReactionDisposer();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
this._textReactionDisposer && this._textReactionDisposer();
- this.pushReactionDisposer && this.pushReactionDisposer();
- this.pullReactionDisposer && this.pullReactionDisposer();
+ this._pushReactionDisposer && this._pushReactionDisposer();
+ this._pullReactionDisposer && this._pullReactionDisposer();
this._heightReactionDisposer && this._heightReactionDisposer();
this._searchReactionDisposer && this._searchReactionDisposer();
+ this._editorView && this._editorView.destroy();
}
-
onPointerDown = (e: React.PointerEvent): void => {
+ 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) {
e.preventDefault();
}
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "0";
- }
}
- let ctrlKey = e.ctrlKey;
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
+ e.preventDefault();
+ }
+ }
+
+ onPointerUp = (e: React.PointerEvent): void => {
+ 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 => {
+ FormattedTextBox.InputBoxOverlay = this;
+ this.tryUpdateHeight();
+ }
+ 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
+ if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+ e.stopPropagation();
+ }
+ }
+
+ static _bulletStyleSheet: any = addStyleSheet();
+ static _userStyleSheet: any = addStyleSheet();
+
+ onClick = (e: React.MouseEvent): void => {
+ 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;
}
- 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) {
- 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"));
-
- }
- }
+ 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);
});
- 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.button === 2 || (e.button === 0 && e.ctrlKey)) {
- e.preventDefault();
- }
- }
- onPointerUp = (e: React.PointerEvent): void => {
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "1";
- }
- if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
- e.stopPropagation();
}
+
+ this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
}
- @action
- onFocused = (e: React.FocusEvent): void => {
- document.removeEventListener("keypress", this.recordKeyHandler);
- document.addEventListener("keypress", this.recordKeyHandler);
- this.tryUpdateHeight();
- if (!this.props.isOverlay) {
- FormattedTextBox.InputBoxOverlay = this;
- } else {
- if (this._ref.current) {
- this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
+ // 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 = hit ? window.getComputedStyle(hit, ':before') : undefined;
+ let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
+ 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 && 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 }));
+ }
+ }
+ }
}
}
}
- 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
- if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
- e.stopPropagation();
- }
- }
+ onMouseUp = (e: React.MouseEvent): void => {
+ e.stopPropagation();
- onClick = (e: React.MouseEvent): void => {
- 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
+ // 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 ((this._editorView as any).mouseDown) {
+ let originalUpHandler = (this._editorView as any).mouseDown.up;
+ (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler);
+ (this._editorView as any).mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).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);
}
});
- //this.props.Document.tooltip = self._toolTipTextMenu;
}
tooltipLinkingMenuPlugin() {
@@ -765,38 +957,34 @@ 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;
}
+ this.doLinkOnDeselect();
}
- public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
}
e.stopPropagation();
- if (e.key === "Tab") e.preventDefault();
- // stop propagation doesn't seem to stop propagation of native keyboard events.
- // so we set a flag on the native event that marks that the event's been handled.
- (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
- if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
- let str = this._editorView.state.doc.textContent;
- let titlestr = str.substr(0, Math.min(40, str.length));
- this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
}
+ 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");
}
- this.tryUpdateHeight();
}
@action
tryUpdateHeight() {
const ChromeHeight = this.props.ChromeHeight;
let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
- if (this.props.Document.autoHeight && sh !== 0) {
+ if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") {
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));
@@ -804,56 +992,40 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- @action
- onPointerEnter = (e: React.PointerEvent) => {
- this._entered = true;
- }
- @action
- onPointerLeave = (e: React.PointerEvent) => {
- this._entered = false;
- }
-
- specificContextMenu = (e: React.MouseEvent): void => {
- // let subitems: ContextMenuProps[] = [];
- // subitems.push({
- // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height",
- // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt"
- // });
- // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" });
- }
-
-
render() {
- let self = this;
- let style = this.props.isOverlay ? "scroll" : "hidden";
+ let style = "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
- let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ||
- (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all";
+ let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
+ ? "none" : "all";
Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ if (this.props.isSelected()) {
+ FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+ }
return (
<div className={`formattedTextBox-cont-${style}`} 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,
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: "13px"
+ fontSize: this._fontSize,
+ fontFamily: this._fontFamily,
}}
+ onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
- onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
- onMouseDown={this.onMouseDown}
+ onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
- onPointerEnter={this.onPointerEnter}
- onPointerLeave={this.onPointerLeave}
+ onPointerEnter={action(() => this._entered = true)}
+ onPointerLeave={action(() => this._entered = false)}
>
- <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
+ <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>
);
}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss
new file mode 100644
index 000000000..792cee182
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.scss
@@ -0,0 +1,34 @@
+.FormattedTextBox-tooltip {
+ position: absolute;
+ pointer-events: none;
+ z-index: 20;
+ background: white;
+ border: 1px solid silver;
+ border-radius: 2px;
+ padding: 2px 10px;
+ margin-bottom: 7px;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+ }
+ .FormattedTextBox-tooltip:before {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -6px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: silver;
+ }
+ .FormattedTextBox-tooltip:after {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -4.5px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: white;
+ } \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
new file mode 100644
index 000000000..a75bfd373
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -0,0 +1,154 @@
+import { Plugin, EditorState } from "prosemirror-state";
+import './FormattedTextBoxComment.scss';
+import { ResolvedPos, Mark } from "prosemirror-model";
+import { EditorView } from "prosemirror-view";
+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 { FormattedTextBox } from "./FormattedTextBox";
+
+export let formattedTextBoxCommentPlugin = new Plugin({
+ view(editorView) { return new FormattedTextBoxComment(editorView); }
+});
+export function findOtherUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+}
+export function findUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid);
+}
+export function findLinkMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.type === schema.marks.link);
+}
+export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let before = 0;
+ let nbef = rpos.nodeBefore;
+ while (nbef && finder(nbef.marks)) {
+ before += nbef.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
+ rpos && (nbef = rpos.nodeBefore);
+ }
+ return before;
+}
+export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let after = 0;
+ let naft = rpos.nodeAfter;
+ while (naft && finder(naft.marks)) {
+ after += naft.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
+ rpos && (naft = rpos.nodeAfter);
+ }
+ return after;
+}
+
+
+export class FormattedTextBoxComment {
+ static tooltip: HTMLElement;
+ static tooltipText: HTMLElement;
+ static start: number;
+ static end: number;
+ static mark: Mark;
+ static opened: boolean;
+ static textBox: FormattedTextBox | undefined;
+ constructor(view: any) {
+ if (!FormattedTextBoxComment.tooltip) {
+ const root = document.getElementById("root");
+ let input = document.createElement("input");
+ input.type = "checkbox";
+ FormattedTextBoxComment.tooltip = document.createElement("div");
+ FormattedTextBoxComment.tooltipText = document.createElement("div");
+ FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
+ FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
+ FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+ FormattedTextBoxComment.tooltip.appendChild(input);
+ FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
+ let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
+ FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
+ FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation(
+ FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
+ FormattedTextBoxComment.opened, keep);
+ };
+ root && root.appendChild(FormattedTextBoxComment.tooltip);
+ }
+ this.update(view, undefined);
+ }
+
+ public static Hide() {
+ FormattedTextBoxComment.textBox = undefined;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
+ }
+ public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) {
+ FormattedTextBoxComment.textBox = textBox;
+ FormattedTextBoxComment.start = start;
+ FormattedTextBoxComment.end = end;
+ FormattedTextBoxComment.mark = mark;
+ FormattedTextBoxComment.opened = opened;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
+ }
+
+ update(view: EditorView, lastState?: EditorState) {
+ 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;
+
+ if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props.isSelected()) return;
+ let set = "none";
+ if (FormattedTextBoxComment.textBox && state.selection.$from) {
+ let 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;
+ if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
+ FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, 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";
+ set = "";
+ }
+ }
+ if (set === "none" && state.selection.$from) {
+ FormattedTextBoxComment.textBox = undefined;
+ let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+ let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
+ let child = state.selection.$from.nodeBefore;
+ let mark = child && findLinkMark(child.marks);
+ if (mark && child && nbef && naft) {
+ FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || 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)));
+ }
+ // 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 = "";
+ }
+ }
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
+ }
+
+ destroy() { FormattedTextBoxComment.tooltip.style.display = "none"; }
+}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 7e78ec684..f3adade58 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -12,6 +12,8 @@ import { IconField } from "../../../new_fields/IconField";
import { ContextMenu } from "../ContextMenu";
import Measure from "react-measure";
import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
+import { Scripting } from "../../util/Scripting";
+import { ComputedField } from "../../../new_fields/ScriptField";
library.add(faCaretUp);
@@ -27,6 +29,25 @@ export class IconBox extends React.Component<FieldViewProps> {
@computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
@computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
+ public static summaryTitleScript(inputDoc: Doc) {
+ const sumDoc = Cast(inputDoc.summaryDoc, Doc) as Doc;
+ if (sumDoc && StrCast(sumDoc.title).startsWith("-")) {
+ return sumDoc.title + ".expanded";
+ }
+ return "???";
+ }
+ public static titleScript(inputDoc: Doc) {
+ const maxDoc = DocListCast(inputDoc.maximizedDocs);
+ if (maxDoc.length === 1) {
+ return maxDoc[0].title + ".icon";
+ }
+ return maxDoc.length > 1 ? "-multiple-.icon" : "???";
+ }
+
+ public static AutomaticTitle(doc: Doc) {
+ Doc.GetProto(doc).title = ComputedField.MakeFunction('iconTitle(this);');
+ }
+
public static DocumentIcon(layout: string) {
let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
layout.indexOf("ImageBox") !== -1 ? faImage :
@@ -38,42 +59,27 @@ export class IconBox extends React.Component<FieldViewProps> {
}
setLabelField = (): void => {
- this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
- }
- setUseOwnTitleField = (): void => {
- this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
+ this.props.Document.hideLabel = !this.props.Document.hideLabel;
}
specificContextMenu = (): void => {
- ContextMenu.Instance.addItem({
- description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
- event: this.setLabelField,
- icon: "tag"
- });
- let maxDocs = DocListCast(this.props.Document.maximizedDocs);
- if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
- ContextMenu.Instance.addItem({
- description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
- event: this.setUseOwnTitleField,
- icon: "text-height"
- });
+ let cm = ContextMenu.Instance;
+ cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" });
+ if (!this.props.Document.hideLabel) {
+ cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
}
}
@observable _panelWidth: number = 0;
@observable _panelHeight: number = 0;
render() {
- let labelField = StrCast(this.props.Document.labelField);
- let hideLabel = BoolCast(this.props.Document.hideLabel);
- let maxDocs = DocListCast(this.props.Document.maximizedDocs);
- let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
- let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle) ? firstDoc[labelField] : this.props.Document.title);
+ 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 }) =>
@@ -82,4 +88,6 @@ export class IconBox extends React.Component<FieldViewProps> {
</Measure>
</div>);
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function iconTitle(doc: any) { return IconBox.titleScript(doc); });
+Scripting.addGlobal(function summaryTitle(doc: any) { return IconBox.summaryTitleScript(doc); }); \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index b1afa3f7d..71d718b39 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,43 +1,72 @@
.imageBox-cont {
- padding: 0vw;
- position: relative;
- text-align: center;
- width: 100%;
- height: auto;
- max-width: 100%;
- max-height: 100%;
- pointer-events: none;
+ padding: 0vw;
+ position: relative;
+ text-align: center;
+ width: 100%;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%;
+ pointer-events: none;
}
+
.imageBox-cont-interactive {
- pointer-events: all;
- width:100%;
- height:auto;
+ pointer-events: all;
+ width: 100%;
+ height: auto;
}
.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;
+}
+
+#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 +78,7 @@
border-radius: 25px;
background: white;
opacity: 0.3;
+
svg {
width: 90% !important;
height: 70%;
@@ -59,43 +89,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%;
+ 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 6fc94a140..a198a0764 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -17,13 +17,12 @@ import { Utils } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
-import { CompileScript } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from '../DocComponent';
import { InkingControl } from '../InkingControl';
-import { positionSchema } from './DocumentView';
+import { documentSchema } from './DocumentView';
import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
@@ -39,6 +38,7 @@ library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
curPage: "number",
+ fitWidth: "boolean"
});
interface Window {
@@ -50,8 +50,8 @@ declare class MediaRecorder {
constructor(e: any);
}
-type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
-const ImageDocument = makeInterface(pageSchema, positionSchema);
+type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
+const ImageDocument = makeInterface(pageSchema, documentSchema);
@observer
export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
@@ -63,10 +63,11 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
private _lastTap: number = 0;
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
+ @observable private hoverActive = false;
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
- @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.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
protected createDropTarget = (ele: HTMLDivElement) => {
if (this.dropDisposer) {
@@ -82,32 +83,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
console.log("IMPLEMENT ME PLEASE");
}
- @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); }
-
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- de.data.droppedDocuments.forEach(action((drop: Doc) => {
- if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
- Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
- e.stopPropagation();
- } else if (de.mods === "MetaKey") {
- if (this.extensionDoc !== this.dataDoc) {
- let layout = StrCast(drop.backgroundLayout);
- if (layout.indexOf(ImageBox.name) !== -1) {
- let imgData = this.extensionDoc.Alternates;
- if (!imgData) {
- Doc.GetProto(this.extensionDoc).Alternates = new List([]);
- }
- let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]);
- imgList && imgList.push(drop);
- e.stopPropagation();
- }
- }
- }
+ if (de.mods === "AltKey" && de.data.draggedDocuments.length && de.data.draggedDocuments[0].data instanceof ImageField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.data.draggedDocuments[0].data.url);
+ e.stopPropagation();
+ }
+ de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => {
+ Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+ e.stopPropagation();
}));
- // de.data.removeDocument() bcz: need to implement
}
}
@@ -216,12 +203,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
- let modes: ContextMenuProps[] = [];
+ let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
+ let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+ !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" });
- ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" });
}
}
@@ -243,9 +231,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
results.tags.map((tag: Tag) => {
tagsList.push(tag.name);
let sanitized = tag.name.replace(" ", "_");
- let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`;
- let computed = CompileScript(script, { params: { this: "Doc" } });
- computed.compiled && (tagDoc[sanitized] = new ComputedField(computed));
+ tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
this.extensionDoc.generatedTags = tagsList;
tagDoc.title = "Generated Tags Doc";
@@ -261,13 +247,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
onDotDown(index: number) {
this.Document.curPage = index;
}
-
- @computed get fieldExtensionDoc() {
- return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
- }
-
@computed private get url() {
- let data = Cast(Doc.GetProto(this.props.Document).data, ImageField);
+ let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField);
return data ? data.url.href : undefined;
}
@@ -318,7 +299,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
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 (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) {
setTimeout(action(() => {
layoutdoc.height = layoutdoc[WidthSym]() * aspect;
layoutdoc.nativeHeight = realsize.height;
@@ -372,6 +353,33 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
this.recordAudioAnnotation();
}
+ considerGooglePhotosLink = () => {
+ const remoteUrl = StrCast(this.props.Document.googlePhotosUrl);
+ if (remoteUrl) {
+ return (
+ <img
+ id={"google-photos"}
+ src={"/assets/google_photos.png"}
+ style={{ opacity: this.hoverActive ? 1 : 0 }}
+ onClick={() => window.open(remoteUrl)}
+ />
+ );
+ }
+ return (null);
+ }
+
+ considerGooglePhotosTags = () => {
+ const tags = StrCast(this.props.Document.googlePhotosTags);
+ if (tags) {
+ return (
+ <img
+ id={"google-tags"}
+ src={"/assets/google_tags.png"}
+ />
+ );
+ }
+ return (null);
+ }
render() {
// let transform = this.props.ScreenToLocalTransform().inverse();
@@ -380,7 +388,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
// let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
// let w = bptX - sptX;
- let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
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")];
@@ -406,11 +413,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
return (
- <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
+ <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
onPointerDown={this.onPointerDown}
+ onPointerEnter={action(() => this.hoverActive = true)}
+ onPointerLeave={action(() => this.hoverActive = false)}
onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<div id="cf">
- <img id={id}
+ <img
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={srcpath}
style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
@@ -434,6 +443,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
<FontAwesomeIcon className="imageBox-audioFont"
style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
</div>
+ {this.considerGooglePhotosLink()}
{/* {this.lightbox(paths)} */}
<FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 653c5c27f..3a9318469 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,26 +2,21 @@
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 { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./KeyValueBox.scss";
-import { KeyValuePair } from "./KeyValuePair";
-import React = require("react");
-import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
-import { Doc, Field, FieldResult, DocListCastAsync } from "../../../new_fields/Doc";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { SetupDrag } from "../../util/DragManager";
-import { Docs } from "../../documents/Documents";
-import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
-import { Templates } from "../Templates";
+import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
-import { TextField } from "../../util/ProsemirrorCopy/prompt";
import { RichTextField } from "../../../new_fields/RichTextField";
-import { ImageField } from "../../../new_fields/URLField";
-import { SelectionManager } from "../../util/SelectionManager";
import { listSpec } from "../../../new_fields/Schema";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { ImageField } from "../../../new_fields/URLField";
+import { Docs } from "../../documents/Documents";
+import { SetupDrag } from "../../util/DragManager";
+import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting";
import { undoBatch } from "../../util/UndoManager";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./KeyValueBox.scss";
+import { KeyValuePair } from "./KeyValuePair";
+import React = require("react");
export type KVPScript = {
script: CompiledScript;
@@ -81,7 +76,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
} else if (type === "script") {
field = new ScriptField(script);
} else {
- let res = script.run({ this: target });
+ let res = script.run({ this: target }, console.log);
if (!res.success) return false;
field = res.result;
}
@@ -129,7 +124,7 @@ 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} ref={(function () {
+ 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);
@@ -203,7 +198,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return;
}
let previousViewType = fieldTemplate.viewType;
- Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
+ Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
previousViewType && (fieldTemplate.viewType = previousViewType);
Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 8001b24a7..1fed4c8bb 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,7 +1,7 @@
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 } from '../../../new_fields/Doc';
+import { Doc, Field, Opt } from '../../../new_fields/Doc';
import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { Transform } from '../../util/Transform';
@@ -22,6 +22,7 @@ export interface KeyValuePairProps {
keyName: string;
doc: Doc;
keyWidth: number;
+ addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -45,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
if (value instanceof Doc) {
e.stopPropagation();
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
}
@@ -55,19 +56,20 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
Document: this.props.doc,
DataDoc: this.props.doc,
ContainingCollectionView: undefined,
+ ContainingCollectionDoc: undefined,
+ ruleProvider: undefined,
fieldKey: this.props.keyName,
fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
renderDepth: 1,
- selectOnLoad: false,
active: returnFalse,
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
- addDocTab: returnZero,
+ addDocTab: returnFalse,
pinToPres: returnZero,
ContentScaling: returnOne
};
@@ -113,7 +115,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<div className="keyValuePair-td-value-container">
<EditableView
contents={contents}
- height={36}
+ maxHeight={36}
+ height={"auto"}
GetValue={() => {
return Field.toKeyValueString(props.Document, props.fieldKey);
}}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index c88a94c28..1c1d6ec95 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,77 +1,98 @@
.pdfBox-cont,
.pdfBox-cont-interactive {
- display: flex;
+ display: inline-block;
flex-direction: row;
height: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
- .pdfBox-scrollHack {
- pointer-events: none;
+ width:100%;
+ overflow: hidden;
+ position:absolute;
+ z-index: -1;
+}
+
+.pdfBox-title-outer {
+ z-index: 0;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: lightslategray;
+ .pdfBox-title-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;
- .pdfPage-textlayer {
- span {
- pointer-events: none !important;
- user-select: none;
+ .collectionFreeFormView-none {
+ pointer-events: none;
+ }
+ .pdfViewer-text {
+ .textLayer {
+ span {
+ user-select: none;
+ }
}
}
}
.pdfBox-cont-interactive {
pointer-events: all;
- .pdfPage-textlayer {
- span {
- pointer-events: all !important;
- user-select: text;
+ .pdfViewer-text {
+ .textLayer {
+ span {
+ user-select: text;
+ }
}
}
}
-.react-pdf__Page {
- transform-origin: left top;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-.react-pdf__Page__textContent span {
- user-select: text;
-}
-
-.react-pdf__Document {
- position: absolute;
-}
-
.pdfBox-settingsCont {
position: absolute;
right: 0;
- top: 0;
+ top: 3;
+ pointer-events: all;
.pdfBox-settingsButton {
border-bottom-left-radius: 50%;
display: flex;
justify-content: space-evenly;
align-items: center;
- height: 70px;
+ height: 30px;
background: none;
padding: 0;
.pdfBox-settingsButton-arrow {
width: 0;
height: 0;
- border-top: 25px solid transparent;
- border-bottom: 25px solid transparent;
- border-right: 25px solid #121721;
+ border-top: 15px solid transparent;
+ border-bottom: 15px solid transparent;
+ border-right: 15px solid #121721;
transition: all 0.5s;
}
.pdfBox-settingsButton-iconCont {
background: #121721;
- height: 50px;
+ height: 30px;
width: 70px;
display: flex;
justify-content: center;
@@ -86,16 +107,15 @@
}
.pdfBox-settingsFlyout {
- width: 600px;
position: absolute;
background: #323232;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
- left: -400px;
+ right: 20px;
border-radius: 7px;
padding: 20px;
display: flex;
flex-direction: column;
- font-size: 30px;
+ font-size: 14px;
transition: all 0.5s;
.pdfBox-settingsFlyout-title {
@@ -108,4 +128,69 @@
grid-template-columns: 47.5% 5% 47.5%;
}
}
-} \ No newline at end of file
+}
+
+.pdfBox-overlayCont {
+ position: absolute;
+ width: 100%;
+ height: 40px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: hidden;
+ transition: left .5s;
+ pointer-events: all;
+
+ .pdfBox-searchBar {
+ width: 70%;
+ font-size: 14px;
+ }
+}
+
+.pdfBox-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 30px;
+ background: none;
+ padding: 0;
+ position: absolute;
+ pointer-events: all;
+
+ .pdfBox-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 15px solid transparent;
+ border-bottom: 15px solid transparent;
+ border-right: 15px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-overlayButton-iconCont,
+ .pdfBox-nextIcon,
+ .pdfBox-prevIcon {
+ background: #121721;
+ height: 30px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+}
+
+.pdfBox-overlayButton:hover {
+ background: none;
+}
+
+.pdfBox-nextIcon {
+ left: 20; top: 5; height: 30px; position: absolute;
+}
+.pdfBox-prevIcon {
+ left: 50; top: 5; height: 30px; position: absolute;
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 18f82ff47..1f3887608 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,119 +1,84 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } 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, WidthSym, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, WidthSym } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast } from "../../../new_fields/Types";
+import { Cast, NumCast } from "../../../new_fields/Types";
import { PdfField } from "../../../new_fields/URLField";
import { KeyCodes } from '../../northstar/utils/KeyCodes';
-import { CompileScript } from '../../util/Scripting';
+import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
import { PDFViewer } from "../pdf/PDFViewer";
-import { positionSchema } from "./DocumentView";
+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';
-type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const PdfDocument = makeInterface(positionSchema, pageSchema);
-export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); };
+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() { return FieldView.LayoutString(PDFBox); }
-
- @observable private _flyout: boolean = false;
- @observable private _alt = false;
- @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
-
- @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }
- @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
- @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); }
-
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
- private _reactionDisposer?: IReactionDisposer;
+ public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
private _keyValue: string = "";
private _valueValue: string = "";
private _scriptValue: string = "";
+ private _searchString: string = "";
+ private _isChildActive = false;
+ 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();
- componentDidMount() {
- this.props.setPdfBox && this.props.setPdfBox(this);
+ @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); }
- const pdfUrl = Cast(this.props.Document.data, PdfField);
+ 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._reactionDisposer = reaction(
- () => this.props.Document.panY,
- () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" })
- );
- }
-
- componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
}
-
- public GetPage() {
- return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;
- }
-
- @action
- public BackPage() {
- let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;
- cp = cp - 1;
- if (cp > 0) {
- this.props.Document.curPage = cp;
- this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
- }
- }
-
- @action
- public GotoPage(p: number) {
- if (p > 0 && p <= NumCast(this.props.Document.numPages)) {
- this.props.Document.curPage = p;
- this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight);
- }
- }
-
- @action
- public ForwardPage() {
- let cp = this.GetPage() + 1;
- if (cp <= NumCast(this.props.Document.numPages)) {
- this.props.Document.curPage = cp;
- this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
- }
+ loaded = (nw: number, nh: number, np: number) => {
+ this.dataDoc.numPages = np;
+ 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));
}
- @action
- setPanY = (y: number) => {
- this.containingCollectionDocument && (this.containingCollectionDocument.panY = y);
- }
+ public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
+ public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); }
+ public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); }
+ public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }
+ public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); };
+ public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }
+ @undoBatch
@action
private applyFilter = () => {
- let scriptText = this._scriptValue.length > 0 ? this._scriptValue :
- this._keyValue.length > 0 && this._valueValue.length > 0 ?
- `return this.${this._keyValue} === ${this._valueValue}` : "return true";
- let script = CompileScript(scriptText, { params: { this: Doc.name } });
- script.compiled && (this.props.Document.filterScript = new ScriptField(script));
- }
-
- scrollTo = (y: number) => {
- this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" });
+ let scriptText = this._scriptValue ? this._scriptValue :
+ this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true";
+ this.props.Document.filterScript = ScriptField.MakeFunction(scriptText);
}
private resetFilters = () => {
- this._keyValue = this._valueValue = "";
- this._scriptValue = "return true";
+ this._keyValue = this._valueValue = this._scriptValue = "";
this._keyRef.current && (this._keyRef.current.value = "");
this._valueRef.current && (this._valueRef.current.value = "");
this._scriptRef.current && (this._scriptRef.current.value = "");
@@ -123,83 +88,125 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value;
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()}
+ 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()}
+ 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) :
- (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}>
- <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings"
- style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}>
- <div className="pdfBox-settingsButton-arrow"
- style={{
- borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
- borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
- borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`,
- transform: `scaleX(${this._flyout ? -1 : 1})`
- }} />
- <div className="pdfBox-settingsButton-iconCont">
- <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" />
- </div>
+ (<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)} />
+ <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()} >
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="sm" />
+ </button>
+ <button className="pdfBox-nextIcon" title="Next Annotation" onClick={e => this.nextAnnotation()} >
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" />
+ </button>
+ </div>
+ <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 8, right: 0 }}>
+ <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
+ <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div>
</button>
- <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} >
- <div className="pdfBox-settingsFlyout-title">
- Annotation View Settings
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange}
- style={{ gridColumn: 1 }} ref={this._keyRef} />
- <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange}
- style={{ gridColumn: 3 }} ref={this._valueRef} />
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
- <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
- &nbsp; Reset Filters
- </button>
- <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
- <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
- &nbsp; Apply
- </button>
+ <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)} />
+ {this._pageControls ? pageBtns : (null)}
+ <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}>
+ <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" >
+ <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} />
+ <div className="pdfBox-settingsButton-iconCont">
+ <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon="cog" size="3x" />
+ </div>
+ </button>
+ <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} >
+ <div className="pdfBox-settingsFlyout-title">
+ Annotation View Settings
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange} style={{ gridColumn: 1 }} ref={this._keyRef} />
+ <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange} style={{ gridColumn: 3 }} ref={this._valueRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
+ &nbsp; Reset Filters
+ </button>
+ <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
+ &nbsp; Apply
+ </button>
+ </div>
</div>
</div>
</div>);
}
- loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc.numPages = np;
- if (!this.dataDoc.nativeWidth || !this.dataDoc.nativeHeight || !this.dataDoc.scrollHeight) {
- let oldaspect = NumCast(this.dataDoc.nativeHeight) / NumCast(this.dataDoc.nativeWidth, 1);
- this.dataDoc.nativeWidth = nw;
- this.dataDoc.nativeHeight = this.dataDoc.nativeHeight ? nw * oldaspect : nh;
- this.dataDoc.height = this.dataDoc[WidthSym]() * (nh / nw);
- this.dataDoc.scrollHeight = np * this.dataDoc.nativeHeight;
- }
- }
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
+ let funcs: ContextMenuProps[] = [];
+ pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
- @action
- onScroll = (e: React.UIEvent<HTMLDivElement>) => {
- if (e.currentTarget && this.containingCollectionDocument) {
- this.containingCollectionDocument.panTransformType = "None";
- this.containingCollectionDocument.panY = e.currentTarget.scrollTop;
- }
+ ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
-
+ _initialScale: number | undefined;
render() {
- const pdfUrl = Cast(this.props.Document.data, PdfField);
- let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
- return (!(pdfUrl instanceof PdfField) || !this._pdf ?
- <div>{`pdf, ${this.props.Document.data}, not found`}</div> :
- <div className={classname}
- onScroll={this.onScroll}
- style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}
- ref={this._mainCont}>
- <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale, 1)), width: "100%" }} />
- <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)}
- Document={this.props.Document} DataDoc={this.props.DataDoc}
- addDocTab={this.props.addDocTab} setPanY={this.setPanY}
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
+ let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
+ let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
+ if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale;
+ if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
+ return (noPdf || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
+ <div className="pdfBox-title-outer" >
+ <div className={classname} >
+ <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
+ </div>
+ </div> :
+ <div className={classname} style={{
+ transformOrigin: "top left",
+ 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: 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} focus={this.props.focus}
pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
- fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} />
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
+ isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
+ fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
{this.settingsPanel()}
</div>);
}
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 e376fbddb..15fafb022 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/CollectionBaseView";
+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,149 +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?
-
-
+export class PresBox extends React.Component<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;
- }
+ _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) {
- if (prevScale !== curScale) {
+ //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);
-
}
/**
@@ -178,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;
}
});
}
@@ -204,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;
}
});
}
@@ -230,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;
}
@@ -259,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;
}
}
@@ -293,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;
+ 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];
}
- if (index < 0 || index >= list.length) {
- return undefined;
- }
-
- 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;
}
@@ -341,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;
-
- if (!this.presStatus) {
- this.presStatus = true;
- this.startPresentation(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;
- 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/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3f4ee8960..e83aa8bea 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,8 +3,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction,
import { observer } from "mobx-react";
import * as rp from 'request-promise';
import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -14,19 +14,21 @@ import { ContextMenuProps } from "../ContextMenuItem";
import { DocComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
-import { positionSchema } from "./DocumentView";
+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 { CompileScript } from "../../util/Scripting";
import { Doc } from "../../../new_fields/Doc";
import { ScriptField } from "../../../new_fields/ScriptField";
+import { positionSchema } from "./CollectionFreeFormDocumentView";
var path = require('path');
-type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(positionSchema, pageSchema);
+export const timeSchema = createSchema({
+ currentTimecode: "number",
+});
+type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
+const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
library.add(faVideo);
@@ -98,11 +100,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);
@@ -113,35 +115,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()
});
- const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, {
- params: { this: Doc.name },
- capturedVariables: { self: this.props.Document },
- typecheck: false,
- editable: true,
- });
- if (script.compiled) {
- b.onClick = new ScriptField(script);
- this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false);
- } else {
- console.log(script.errors.map(error => error.messageText).join("\n"));
- }
+ 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, "");
- VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
+ let filename = path.basename(encodeURIComponent("snapshot" + this.Document.title + "_" + (this.Document.currentTimecode || 0).toString()));
+ VideoBox.convertDataUri(dataUrl, filename.replace(/\..*$/, "")).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-"
});
+ imageSummary.isButton = true;
this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
- DocUtils.MakeLink(imageSummary, this.props.Document);
+ DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot");
}
});
}
@@ -149,8 +141,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() {
@@ -158,12 +150,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.height = (this.Document.width || 0) / youtubeaspect;
}
}
}
@@ -180,10 +172,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 });
}
}
@@ -204,7 +195,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
}
specificContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
if (field) {
let url = field.url.href;
let subitems: ContextMenuProps[] = [];
@@ -216,7 +207,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@computed get content() {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
@@ -228,7 +219,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@computed get youtubeVideoId() {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
}
@@ -254,7 +245,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
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";
@@ -269,18 +260,21 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
+ @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
@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)}
+ 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}`}
></iframe>;
}
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>;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 43220df71..fbe9bf063 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -30,6 +30,7 @@
width: 100%;
height: 100%;
position: absolute;
+ pointer-events: all;
}
.webBox-button {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f0140d04b..29eef27a0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,24 +1,29 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { FieldResult, Doc } from "../../../new_fields/Doc";
+import { FieldResult, Doc, Field } from "../../../new_fields/Doc";
import { HtmlField } from "../../../new_fields/HtmlField";
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
-import { Utils } from "../../../Utils";
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 { faStickyNote } from "@fortawesome/free-solid-svg-icons";
-import { library } from "@fortawesome/fontawesome-svg-core";
-library.add(faStickyNote)
+library.add(faStickyNote);
@observer
export class WebBox extends React.Component<FieldViewProps> {
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 6f77a0a5b..5a07b88d9 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,24 +1,20 @@
import React = require("react");
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } 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";
import { DocumentManager } from "../../util/DocumentManager";
import PDFMenu from "./PDFMenu";
import "./Annotation.scss";
-import { scale } from "./PDFViewer";
-import { PresBox } from "../nodes/PresBox";
interface IAnnotationProps {
anno: Doc;
- index: number;
- ParentIndex: () => number;
fieldExtensionDoc: Doc;
- scrollTo?: (n: number) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ 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> {
@@ -33,11 +29,8 @@ interface IRegionAnnotationProps {
y: number;
width: number;
height: number;
- index: number;
- ParentIndex: () => number;
fieldExtensionDoc: Doc;
- scrollTo?: (n: number) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
document: Doc;
}
@@ -45,9 +38,11 @@ interface IRegionAnnotationProps {
@observer
class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
private _reactionDisposer?: IReactionDisposer;
- private _scrollDisposer?: IReactionDisposer;
+ private _brushDisposer?: IReactionDisposer;
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ @observable private _brushed: boolean = false;
+
componentDidMount() {
this._reactionDisposer = reaction(
() => this.props.document.delete,
@@ -55,15 +50,19 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
{ fireImmediately: true }
);
- this._scrollDisposer = reaction(
- () => this.props.ParentIndex(),
- (ind) => ind === this.props.index && this.props.scrollTo && this.props.scrollTo(this.props.y * scale)
+ this._brushDisposer = reaction(
+ () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.isBrushedHighlightedDegree(FieldValue(Cast(this.props.document.group, Doc))!),
+ (brushed) => {
+ if (brushed !== undefined) {
+ runInAction(() => this._brushed = brushed !== 0);
+ }
+ }
);
}
componentWillUnmount() {
+ this._brushDisposer && this._brushDisposer();
this._reactionDisposer && this._reactionDisposer();
- this._scrollDisposer && this._scrollDisposer();
}
deleteAnnotation = () => {
@@ -88,27 +87,26 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
@action
onPointerDown = async (e: React.PointerEvent) => {
- 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);
- }
- }
- }
- if (e.button === 2) {
+ if (e.button === 2 || e.ctrlKey) {
PDFMenu.Instance.Status = "annotation";
PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
PDFMenu.Instance.Pinned = false;
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 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 ? "onRight" : "inTab"),
+ false, false, undefined);
+ }
}
}
+
addTag = (key: string, value: string): boolean => {
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
@@ -126,7 +124,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
left: this.props.x,
width: this.props.width,
height: this.props.height,
- backgroundColor: this.props.ParentIndex() === this.props.index ? "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/PDFAnnotationLayer.scss b/src/client/views/pdf/PDFAnnotationLayer.scss
deleted file mode 100644
index 733533007..000000000
--- a/src/client/views/pdf/PDFAnnotationLayer.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.pdfAnnotationLayer-cont {
- width:100%;
- height:100%;
- position:relative;
- top:-200%;
-} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
deleted file mode 100644
index 4f267a5c0..000000000
--- a/src/client/views/pdf/PDFAnnotationLayer.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import "./PDFAnnotationLayer.scss";
-
-interface IAnnotationProps {
-
-}
-
-@observer
-export class PDFAnnotationLayer extends React.Component {
- onPointerDown = (e: React.PointerEvent) => {
- if (e.ctrlKey) {
- console.log("annotating");
- e.stopPropagation();
- }
- }
-
- render() {
- return <div className="pdfAnnotationLayer-cont" onPointerDown={this.onPointerDown} />;
- }
-} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 3ed81faef..517a99a68 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -1,11 +1,10 @@
import React = require("react");
import "./PDFMenu.scss";
-import { observable, action, runInAction } from "mobx";
+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 { handleBackspace } from "../nodes/PDFBox";
+import { Doc, Opt } from "../../../new_fields/Doc";
@observer
export default class PDFMenu extends React.Component {
@@ -32,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;
@@ -156,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");
}
}
@@ -238,8 +233,8 @@ export default class PDFMenu extends React.Component {
<button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}>
<FontAwesomeIcon icon="map-pin" size="lg" /></button>,
<div key="7" className="pdfMenu-addTag" >
- <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
- <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+ <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+ <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
</div>,
<button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
<FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a2f3911c5..f6fedf3da 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,93 +1,77 @@
-
-.pdfViewer-viewer {
- pointer-events:inherit;
+
+.pdfViewer-viewer, .pdfViewer-viewer-zoomed {
+ pointer-events: inherit;
width: 100%;
- .pdfViewer-visibleElements {
- .pdfPage-cont {
- .pdfPage-textLayer {
- div {
- user-select: text;
- }
- span {
- color: transparent;
- position: absolute;
- white-space: pre;
- cursor: text;
- -webkit-transform-origin: 0% 0%;
- transform-origin: 0% 0%;
- }
- }
+ height: 100%;
+ position: absolute;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ // .canvasWrapper {
+ // transform: scale(0.75);
+ // transform-origin: top left;
+ // }
+ .textLayer {
+
+ mix-blend-mode: multiply;
+ opacity: 0.9;
+ span {
+ padding-right: 5px;
+ padding-bottom: 4px;
}
}
- .pdfViewer-text {
- transform: scale(1.5);
- transform-origin: top left;
+ .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation()
+ .textLayer .highlight {
+ background-color: yellow;
+ }
+ .textLayer .highlight.selected {
+ background-color: orange;
}
- .pdfViewer-annotationLayer {
- position: absolute;
- top: 0;
- width: 100%;
+ .page {
+ position: relative;
+ }
+ .collectionfreeformview-container {
pointer-events: none;
- .pdfPage-annotationBox {
- position: absolute;
- background-color: red;
- opacity: 0.1;
- }
}
- .pdfViewer-overlayCont {
- position: absolute;
- width: 100%;
- height: 100px;
- background: #121721;
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 20px;
- overflow: hidden;
- transition: left .5s;
- .pdfViewer-overlaySearchBar {
- width: 20%;
- height: 100%;
- font-size: 30px;
- padding: 5px;
- }
+ .pdfViewer-dragAnnotationBox {
+ position:absolute;
+ background-color: transparent;
+ opacity: 0.1;
}
- .pdfViewer-overlayButton {
- border-bottom-left-radius: 50%;
- display: flex;
- justify-content: space-evenly;
- align-items: center;
- height: 70px;
- background: none;
- padding: 0;
+ .pdfViewer-overlay {
+ transform-origin: left top;
position: absolute;
+ top: 0px;
+ left: 0px;
+ display: inline-block;
+ width:100%;
+ }
+ .pdfViewer-annotationLayer {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ pointer-events: none;
+ mix-blend-mode: multiply;
- .pdfViewer-overlayButton-arrow {
- width: 0;
- height: 0;
- border-top: 25px solid transparent;
- border-bottom: 25px solid transparent;
- border-right: 25px solid #121721;
- transition: all 0.5s;
- }
-
- .pdfViewer-overlayButton-iconCont {
- background: #121721;
- height: 50px;
- width: 70px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-left: -2px;
- border-radius: 3px;
+ .pdfViewer-annotationBox {
+ position: absolute;
+ background-color: rgba(245, 230, 95, 0.616);
}
}
-
- .pdfViewer-overlayButton:hover {
- background: none;
+ .pdfViewer-waiting {
+ width: 70%;
+ height: 70%;
+ margin : 15%;
+ transition: 0.4s opacity ease;
+ opacity: 0.7;
+ position: absolute;
+ z-index: 10;
}
}
+.pdfViewer-viewer-zoomed {
+ overflow-x: scroll;
+}
+ \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 258e218f0..b010d16c8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,27 +1,34 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import * as rp from "request-promise";
import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, FieldResult } from "../../../new_fields/Doc";
+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 { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, numberRange } from "../../../Utils";
-import { DocServer } from "../../DocServer";
+import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
-import { KeyCodes } from "../../northstar/utils/KeyCodes";
-import { CompileScript, CompiledScript } from "../../util/Scripting";
-import Annotation from "./Annotation";
-import Page from "./Page";
+import { DragManager } from "../../util/DragManager";
+import { CompiledScript, CompileScript } from "../../util/Scripting";
+import { Transform } from "../../util/Transform";
+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";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
+const pdfjsLib = require("pdfjs-dist");
-export const scale = 2;
+pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
interface IViewerProps {
pdf: Pdfjs.PDFDocumentProxy;
@@ -30,14 +37,25 @@ interface IViewerProps {
DataDoc?: Doc;
fieldExtensionDoc: Doc;
fieldKey: string;
+ fieldExt: string;
+ 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;
- panY: number;
- scrollTo: (y: number) => void;
active: () => boolean;
- setPanY?: (n: number) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ GoToPage?: (n: number) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ setPdfViewer: (view: PDFViewer) => void;
+ ScreenToLocalTransform: () => Transform;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ whenActiveChanged: (isActive: boolean) => void;
}
/**
@@ -45,151 +63,204 @@ interface IViewerProps {
*/
@observer
export class PDFViewer extends React.Component<IViewerProps> {
- @observable.shallow private _visibleElements: JSX.Element[] = []; // _visibleElements is the array of JSX elements that gets rendered
- @observable private _isPage: string[] = [];// _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder
+ 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[]>();
@observable private _script: CompiledScript = CompileScript("return true") as CompiledScript;
- @observable private _searching: boolean = false;
@observable private Index: number = -1;
+ @observable private _marqueeX: number = 0;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _marqueeing: boolean = false;
+ @observable private _showWaiting = true;
+ @observable private _showCover = false;
+ @observable private _zoomed = 1;
+ @observable private _scrollTop = 0;
- private _pageBuffer: number = 1;
+ private _pdfViewer: any;
+ private _retries = 0; // number of times tried to create the PDF viewer
+ private _isChildActive = false;
+ 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 _pdfViewer: any;
- private _pdfFindController: any;
- private _searchString: string = "";
private _selectionText: string = "";
-
- @computed get panY(): number { return this.props.panY; }
-
- // startIndex: where to start rendering pages
- @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.panY) - this._pageBuffer); }
-
- // endIndex: where to end rendering pages
- @computed get endIndex(): number {
- return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.panY + (this._pageSizes[0] ? this._pageSizes[0].height : 0)) + this._pageBuffer);
+ private _startX: number = 0;
+ private _startY: number = 0;
+ private _downX: number = 0;
+ private _downY: number = 0;
+ private _coverPath: any;
+
+ @computed get allAnnotations() {
+ return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
+ anno => this._script.run({ this: anno }, console.log, true).result);
}
- @computed get filteredAnnotations() {
- return this._annotations.filter(anno => {
- let run = this._script.run({ this: anno });
- return run.success ? run.result : true;
- });
+ @computed get nonDocAnnotations() {
+ return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
}
- componentDidUpdate = (prevProps: IViewerProps) => this.panY !== prevProps.panY && this.renderPages();
-
+ _lastSearch: string = "";
componentDidMount = async () => {
- await this.initialLoad();
+ // 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`)));
+ runInAction(() => this._showWaiting = this._showCover = true);
+ this.props.startupLive && this.setupPdfJsViewer();
+ this._searchReactionDisposer = reaction(() => StrCast(this.props.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(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true });
this._reactionDisposer = reaction(
- () => [this.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0],
- () => this.renderPages(),
- { fireImmediately: true });
-
- this._annotationReactionDisposer = reaction(
- () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
- annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
- { fireImmediately: true });
-
- this._filterReactionDisposer = reaction(
- () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
- action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
- this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
- annos.forEach(d => {
- let run = this._script.run(d);
- d.opacity = !run.success || run.result ? 1 : 0;
- });
- this.Index = -1;
- }),
+ () => this.props.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;
+ }
+ },
{ fireImmediately: true }
);
-
- document.removeEventListener("copy", this.copy);
- document.addEventListener("copy", this.copy);
}
componentWillUnmount = () => {
this._reactionDisposer && this._reactionDisposer();
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)));
- }
- }
-
- searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
-
- pageLoaded = (page: Pdfjs.PDFPageViewport): void => this.props.loaded(page.width, page.height, this.props.pdf.numPages);
-
- setSelectionText = (text: string) => this._selectionText = text;
-
- getIndex = () => this.Index;
-
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
- this._isPage = Array<string>(this.props.pdf.numPages);
this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) =>
this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => {
this._pageSizes.splice(i, 1, {
- width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,
- height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale
+ width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
+ height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0])
});
- this.getPlaceholderPage(i);
+ 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);
}))));
- this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages);
+ Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
+ }
+ }
+
+ @action
+ setupPdfJsViewer = async () => {
+ this._selectionReactionDisposer && this._selectionReactionDisposer();
+ this._selectionReactionDisposer = undefined;
+ this._showWaiting = true;
+ this.props.setPdfViewer(this);
+ await this.initialLoad();
- let startY = NumCast(this.props.Document.startY, NumCast(this.props.Document.panY));
- this.props.setPanY && this.props.setPanY(startY);
+ this._annotationReactionDisposer = reaction(
+ () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
+ annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
+ { fireImmediately: true });
+
+ this._filterReactionDisposer = reaction(
+ () => ({ scriptField: Cast(this.props.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;
+ if (this._script.originalScript !== oldScript) {
+ this.Index = -1;
+ }
+ annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0);
+ }),
+ { fireImmediately: true }
+ );
+
+ this.createPdfViewer();
+ }
+
+ createPdfViewer() {
+ if (!this._mainCont.current) {
+ 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));
+ }));
+ 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({
+ container: this._mainCont.current,
+ viewer: this._viewer.current,
+ linkService: pdfLinkService,
+ findController: pdfFindController,
+ renderer: "canvas",
+ });
+ pdfLinkService.setViewer(this._pdfViewer);
+ pdfLinkService.setDocument(this.props.pdf, null);
+ 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 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 " + StrCast(this.props.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();
- this.props.addDocument && this.props.addDocument(annoDoc, false);
mainAnnoDoc = annoDoc;
- mainAnnoDocProto = Doc.GetProto(annoDoc);
+ mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
+ mainAnnoDocProto.y = annoDoc.y;
} else {
this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => {
let annoDoc = new Doc();
@@ -197,10 +268,8 @@ 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));
@@ -211,70 +280,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.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
- getPlaceholderPage = (page: number) => {
- if (this._isPage[page] !== "none") {
- this._isPage[page] = "none";
- this._visibleElements[page] = (
- <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder"
- style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }}>
- "PAGE IS LOADING... "
- </div>);
- }
- }
-
- @action
- getRenderedPage = (page: number) => {
- if (this._isPage[page] !== "page") {
- this._isPage[page] = "page";
- this._visibleElements[page] = (<Page {...this.props}
- size={this._pageSizes[page]}
- numPages={this.props.pdf.numPages}
- setSelectionText={this.setSelectionText}
- page={page}
- key={`${this.props.url}-rendered-${page + 1}`}
- name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`}
- pageLoaded={this.pageLoaded}
- renderAnnotations={this.renderAnnotations}
- createAnnotation={this.createAnnotation}
- sendAnnotations={this.receiveAnnotations}
- makeAnnotationDocuments={this.makeAnnotationDocument}
- getScrollFromPage={this.getScrollFromPage} />);
- }
- }
-
- // change the address to be the file address of the PNG version of each page
- // file address of the pdf
- @action
- getPageImage = async (page: number) => {
- if (this._isPage[page] !== "image") {
- this._isPage[page] = "image";
- try {
- let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${page + 1}.PNG`)));
- runInAction(() => this._visibleElements[page] =
- <img key={res.path} src={res.path} onError={() => this.getRenderedPage(page)}
- style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
- } catch (e) {
- console.log(e);
- }
- }
- }
-
- renderPages = () => {
- numberRange(this.props.pdf.numPages).filter(p => this._isPage[p] !== undefined).map(i =>
- (i < this.startIndex || i > this.endIndex) ? this.getPlaceholderPage(i) : // pages outside of the pdf use empty stand-in divs
- this.props.active() ? this.getRenderedPage(i) : this.getPageImage(i));
- }
-
- @action
renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
if (removeOldAnnotations) {
this._annotations = annotations;
@@ -286,29 +297,34 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- prevAnnotation = (e: React.MouseEvent) => {
- e.stopPropagation();
+ prevAnnotation = () => {
this.Index = Math.max(this.Index - 1, 0);
+ this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]);
}
@action
- nextAnnotation = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.Index = Math.min(this.Index + 1, this.filteredAnnotations.length - 1);
+ nextAnnotation = () => {
+ this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1);
+ this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]);
}
- sendAnnotations = (page: number) => {
- return this._savedAnnotations.getValue(page);
+ @action
+ gotoPage = (p: number) => {
+ this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
- 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);
- }
+ @action
+ scrollToAnnotation = (scrollToAnnotation: Doc) => {
+ 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._scrollTop = this._mainCont.current!.scrollTop;
+ this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber);
}
// get the page index that the vertical offset passed in is on
@@ -321,17 +337,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
return index;
}
- getScrollFromPage = (index: number): number => {
- return numberRange(Math.min(this.props.pdf.numPages, index)).reduce((counter, i) => counter + this._pageSizes[i].height, 0);
- }
-
@action
createAnnotation = (div: HTMLDivElement, page: number) => {
if (this._annotationLayer.current) {
if (div.style.top) {
- div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString();
+ 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);
@@ -344,125 +358,360 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- search = (searchString: string) => {
- if (this._pdfViewer._pageViewsReady) {
- this._pdfFindController.executeCommand('find', {
+ search = (searchString: string, fwd: boolean) => {
+ if (!searchString) {
+ fwd ? this.nextAnnotation() : this.prevAnnotation();
+ }
+ else if (this._pdfViewer._pageViewsReady) {
+ this._pdfViewer.findController.executeCommand('findagain', {
caseSensitive: false,
- findPrevious: undefined,
+ findPrevious: !fwd,
highlightAll: true,
phraseSearch: true,
query: searchString
});
}
else if (this._mainCont.current) {
- let executeFind = () => this._pdfFindController.executeCommand('find', {
- caseSensitive: false,
- findPrevious: undefined,
- highlightAll: true,
- phraseSearch: true,
- query: searchString
- });
+ let executeFind = () => {
+ this._pdfViewer.findController.executeCommand('find', {
+ caseSensitive: false,
+ findPrevious: !fwd,
+ highlightAll: true,
+ phraseSearch: true,
+ query: searchString
+ });
+ };
this._mainCont.current.addEventListener("pagesloaded", executeFind);
this._mainCont.current.addEventListener("pagerendered", executeFind);
}
}
-
@action
- toggleSearch = (e: React.MouseEvent) => {
- e.stopPropagation();
- this._searching = !this._searching;
-
- if (this._searching) {
- if (!this._pdfFindController && this._mainCont.current && this._viewer.current) {
- let simpleLinkService = new SimpleLinkService();
- this._pdfViewer = new PDFJSViewer.PDFViewer({
- container: this._mainCont.current,
- viewer: this._viewer.current,
- linkService: simpleLinkService
- });
- simpleLinkService.setPdf(this.props.pdf);
- this._mainCont.current.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1);
- this._mainCont.current.addEventListener("pagerendered", () => console.log("rendered"));
- this._pdfViewer.setDocument(this.props.pdf);
- this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
- this._pdfViewer.findController = this._pdfFindController;
+ onPointerDown = (e: React.PointerEvent): void => {
+ // if alt+left click, drag and annotate
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
+ if (NumCast(this.props.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") {
+ // start selecting text if mouse down on textLayer spans
+ }
+ else if (this._mainCont.current) {
+ // set marquee x and y positions to the spatially transformed position
+ 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;
}
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.addEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
}
- else {
- this._pdfFindController = null;
- if (this._viewer.current) {
- let cns = this._viewer.current.childNodes;
- for (let i = cns.length - 1; i >= 0; i--) {
- cns.item(i).remove();
+ }
+
+ @action
+ onSelectMove = (e: PointerEvent): void => {
+ if (this._marqueeing && this._mainCont.current) {
+ // transform positions and find the width and height to set the marquee to
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX;
+ this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop;
+ this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
+ this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
+ this._marqueeWidth = Math.abs(this._marqueeWidth);
+ this._marqueeHeight = Math.abs(this._marqueeHeight);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (e.target && (e.target as any).parentElement === this._mainCont.current) {
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ createTextAnnotation = (sel: Selection, selRange: Range) => {
+ if (this._mainCont.current) {
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ let clientRects = selRange.getClientRects();
+ for (let i = 0; i < clientRects.length; i++) {
+ let rect = clientRects.item(i);
+ 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 &&
+ (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
+ let annoBox = document.createElement("div");
+ 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();
+ annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width).toString();
+ annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height).toString();
+ this.createAnnotation(annoBox, this.getPageFromScroll(rect.top));
+ }
}
}
}
+ this._selectionText = selRange.cloneContents().textContent || "";
+
+ // clear selection
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
}
- render() {
- return (<div className="pdfViewer-viewer" ref={this._mainCont} >
- <div className="pdfViewer-visibleElements" style={this._searching ? { position: "absolute", top: 0 } : {}}>
- {this._visibleElements}
- </div>
- <div className="pdfViewer-text" ref={this._viewer} />
- <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
- {this.filteredAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} ParentIndex={this.getIndex} anno={anno} index={index} key={`${anno[Id]}-annotation`} />)}
- </div>
- <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()}
- style={{ bottom: -this.props.panY, left: `${this._searching ? 0 : 100}%` }}>
- <button className="pdfViewer-overlayButton" title="Open Search Bar" />
- <input className="pdfViewer-overlaySearchBar" placeholder="Search" onChange={this.searchStringChanged}
- onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} />
- <button title="Search" onClick={() => this.search(this._searchString)}>
- <FontAwesomeIcon icon="search" size="3x" color="white" /></button>
- </div>
- <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation"
- style={{ bottom: -this.props.panY + 280, right: 10, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /></div>
- </button>
- <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation"
- style={{ bottom: -this.props.panY + 200, right: 10, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /></div>
- </button>
- <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar"
- style={{ bottom: -this.props.panY + 10, right: 0, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /></div>
- </button>
- </div >);
+ @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) { // copy the marquee and convert it to a permanent annotation.
+ let style = (marquees[0] as HTMLDivElement).style;
+ let copy = document.createElement("div");
+ 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 as any).marqueeing = true;
+ copy.className = "pdfViewer-annotationBox";
+ this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY));
+ }
+
+ if (!e.ctrlKey) {
+ PDFMenu.Instance.Status = "snippet";
+ PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+ }
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ this._marqueeing = false;
+ }
+ else {
+ let sel = window.getSelection();
+ if (sel && sel.type === "Range") {
+ let selRange = sel.getRangeAt(0);
+ this.createTextAnnotation(sel, selRange);
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ }
+
+ 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;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
}
-}
-export enum AnnotationTypes { Region }
+ @action
+ highlight = (color: string) => {
+ // creates annotation documents for current highlights
+ let annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+ return annotationDoc;
+ }
-class SimpleLinkService {
- externalLinkTarget: any = null;
- externalLinkRel: any = null;
- pdf: any = null;
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ startDrag = (e: PointerEvent, ele: HTMLElement): void => {
+ e.preventDefault();
+ e.stopPropagation();
+ 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 ${StrCast(this.props.Document.title)}`, "link from PDF")
+
+ },
+ hideSource: false
+ });
+ }
+ }
- navigateTo() { }
+ createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
+ let view = Doc.MakeAlias(this.props.Document);
+ let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
+ 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.startY = marquee.top;
+ view.width = this.props.Document[WidthSym]();
+ DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
+ }
- getDestinationHash() { return "#"; }
+ // 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;
+ }
- getAnchorUrl() { return "#"; }
- setHash() { }
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = undefined;
+ //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._scrollTop) : this.props.ScreenToLocalTransform();
+ }
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
+ this._setPreviewCursor = func;
+ }
+ onClick = (e: React.MouseEvent) => {
+ this._setPreviewCursor &&
+ e.button === 0 &&
+ Math.abs(e.clientX - this._downX) < 3 &&
+ 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;
+ }
- executeNamedAction() { }
+ getCoverImage = () => {
+ if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
+ 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;
+ }).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} 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` }} />;
+ }
- cachePageRef() { }
- get pagesCount() { return this.pdf ? this.pdf.numPages : 0; }
+ @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);
+ }
+ }
- get page() { return 0; }
+ @computed get annotationLayer() {
+ return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
+ {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
+ <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
+ <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)}
+ VisibleHeight={this.visibleHeight}
+ 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={(doc: Doc, allow: boolean | undefined) => { Doc.GetProto(doc).annotationOn = this.props.Document; 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>
+ </div>;
+ }
+ @computed get pdfViewerDiv() {
+ return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
+ }
+ @computed get standinViews() {
+ return <>
+ {this._showCover ? this.getCoverImage() : (null)}
+ {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
+ </>;
+ }
+ marqueeWidth = () => this._marqueeWidth;
+ marqueeHeight = () => this._marqueeHeight;
+ marqueeX = () => this._marqueeX;
+ marqueeY = () => this._marqueeY;
+ marqueeing = () => this._marqueeing;
+ visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
+ render() {
+ 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}
+ {this.annotationLayer}
+ {this.standinViews}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+ </div >);
+ }
+}
- setPdf(pdf: any) { this.pdf = pdf; }
+interface PdfViewerMarqueeProps {
+ isMarqueeing: () => boolean;
+ width: () => number;
+ height: () => number;
+ x: () => number;
+ y: () => number;
+}
- get rotation() { return 0; }
- set rotation(value: any) { }
+@observer
+class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
+ render() {
+ return !this.props.isMarqueeing() ? (null) : <div className="pdfViewer-dragAnnotationBox"
+ 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"}`,
+ opacity: 0.2
+ }}>
+ </div>;
+ }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.scss b/src/client/views/pdf/Page.scss
deleted file mode 100644
index af1628a6f..000000000
--- a/src/client/views/pdf/Page.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-
-.pdfPage-cont {
- position: relative;
-
- .pdfPage-canvasContainer {
- position: absolute;
- }
-
- .pdfPage-dragAnnotationBox {
- position: absolute;
- background-color: transparent;
- opacity: 0.1;
- }
-
- .pdfPage-textLayer {
- position: absolute;
- width: 100%;
- height: 100%;
- div {
- user-select: text;
- }
- span {
- color: transparent;
- position: absolute;
- white-space: pre;
- cursor: text;
- -webkit-transform-origin: 0% 0%;
- transform-origin: 0% 0%;
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
deleted file mode 100644
index 6b039a6bc..000000000
--- a/src/client/views/pdf/Page.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-import { action, IReactionDisposer, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as Pdfjs from "pdfjs-dist";
-import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DragManager } from "../../util/DragManager";
-import PDFMenu from "./PDFMenu";
-import { scale } from "./PDFViewer";
-import "./Page.scss";
-import React = require("react");
-
-
-interface IPageProps {
- size: { width: number, height: number };
- pdf: Pdfjs.PDFDocumentProxy;
- name: string;
- numPages: number;
- page: number;
- pageLoaded: (page: Pdfjs.PDFPageViewport) => void;
- fieldExtensionDoc: Doc;
- Document: Doc;
- renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
- sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
- createAnnotation: (div: HTMLDivElement, page: number) => void;
- makeAnnotationDocuments: (doc: Doc | undefined, color: string, linkTo: boolean) => Doc;
- getScrollFromPage: (page: number) => number;
- setSelectionText: (text: string) => void;
-}
-
-@observer
-export default class Page extends React.Component<IPageProps> {
- @observable private _state: "N/A" | "rendering" = "N/A";
- @observable private _width: number = this.props.size.width;
- @observable private _height: number = this.props.size.height;
- @observable private _page: Opt<Pdfjs.PDFPageProxy>;
- @observable private _currPage: number = this.props.page + 1;
- @observable private _marqueeX: number = 0;
- @observable private _marqueeY: number = 0;
- @observable private _marqueeWidth: number = 0;
- @observable private _marqueeHeight: number = 0;
-
- private _canvas: React.RefObject<HTMLCanvasElement> = React.createRef();
- private _textLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _marquee: React.RefObject<HTMLDivElement> = React.createRef();
- private _marqueeing: boolean = false;
- private _reactionDisposer?: IReactionDisposer;
- private _startY: number = 0;
- private _startX: number = 0;
-
- componentDidMount = (): void => this.loadPage(this.props.pdf);
-
- componentDidUpdate = (): void => this.loadPage(this.props.pdf);
-
- componentWillUnmount = (): void => this._reactionDisposer && this._reactionDisposer();
-
- loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => {
- pdf.getPage(this._currPage).then(page => this.renderPage(page));
- }
-
- @action
- renderPage = (page: Pdfjs.PDFPageProxy): void => {
- // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
- if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) {
- this._state = "rendering";
- let viewport = page.getViewport(scale as any);
- this._canvas.current.width = this._width = viewport.width;
- this._canvas.current.height = this._height = viewport.height;
- this.props.pageLoaded(viewport);
- let ctx = this._canvas.current.getContext("2d");
- if (ctx) {
- page.render({ canvasContext: ctx, viewport: viewport }); // renders the page onto the canvas context
- page.getTextContent().then(res => // renders text onto the text container
- //@ts-ignore
- Pdfjs.renderTextLayer({
- textContent: res,
- container: this._textLayer.current,
- viewport: viewport
- }));
-
- this._page = page;
- }
- }
- }
-
- @action
- highlight = (targetDoc: Doc | undefined, color: string) => {
- // creates annotation documents for current highlights
- let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, color, false);
- Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc);
- return annotationDoc;
- }
-
- /**
- * This is temporary for creating annotations from highlights. It will
- * start a drag event and create or put the necessary info into the drag event.
- */
- @action
- startDrag = (e: PointerEvent, ele: HTMLElement): void => {
- e.preventDefault();
- e.stopPropagation();
- if (this._textLayer.current) {
- let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
- targetDoc.targetPage = this.props.page;
- 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: async () => {
- if (!BoolCast(annotationDoc.linkedToDoc)) {
- let annotations = await DocListCastAsync(annotationDoc.annotations);
- annotations && annotations.forEach(anno => anno.target = targetDoc);
- DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
- }
- },
- hideSource: false
- });
- }
- }
-
- // cleans up events and boolean
- endDrag = (e: PointerEvent): void => {
- e.stopPropagation();
- }
-
- createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
- let view = Doc.MakeAlias(this.props.Document);
- let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
- 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.startY = marquee.top + this.props.getScrollFromPage(this.props.page);
- view.width = this.props.Document[WidthSym]();
- DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view], [undefined]), 0, 0);
- }
-
- @action
- onPointerDown = (e: React.PointerEvent): void => {
- // if alt+left click, drag and annotate
- if (NumCast(this.props.Document.scale, 1) !== 1) return;
- if (!e.altKey && e.button === 0) {
- PDFMenu.Instance.StartDrag = this.startDrag;
- PDFMenu.Instance.Highlight = this.highlight;
- PDFMenu.Instance.Snippet = this.createSnippet;
- PDFMenu.Instance.Status = "pdf";
- PDFMenu.Instance.fadeOut(true);
- if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- e.stopPropagation();
- if (!e.ctrlKey) {
- this.props.sendAnnotations([], -1);
- }
- }
- else {
- // set marquee x and y positions to the spatially transformed position
- if (this._textLayer.current) {
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width);
- this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height);
- }
- this._marqueeing = true;
- this._marquee.current && (this._marquee.current.style.opacity = "0.2");
- this.props.sendAnnotations([], -1);
- }
- document.removeEventListener("pointermove", this.onSelectStart);
- document.addEventListener("pointermove", this.onSelectStart);
- document.removeEventListener("pointerup", this.onSelectEnd);
- document.addEventListener("pointerup", this.onSelectEnd);
- }
- }
-
- @action
- onSelectStart = (e: PointerEvent): void => {
- if (this._marqueeing && this._textLayer.current) {
- // transform positions and find the width and height to set the marquee to
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)) - this._startX;
- this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)) - this._startY;
- this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
- this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
- this._marqueeWidth = Math.abs(this._marqueeWidth);
- e.stopPropagation();
- e.preventDefault();
- }
- else if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- e.stopPropagation();
- }
- }
-
- @action
- onSelectEnd = (e: PointerEvent): void => {
- if (this._marqueeing) {
- this._marqueeing = false;
- if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
- if (this._marquee.current) { // make a copy of the marquee
- let copy = document.createElement("div");
- let style = this._marquee.current.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";
- this.props.createAnnotation(copy, this.props.page);
- this._marquee.current.style.opacity = "0";
- }
-
- if (!e.ctrlKey) {
- PDFMenu.Instance.Status = "snippet";
- PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
- }
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
- }
-
- this._marqueeHeight = this._marqueeWidth = 0;
- }
- else {
- let sel = window.getSelection();
- if (sel && sel.type === "Range") {
- let selRange = sel.getRangeAt(0);
- this.createTextAnnotation(sel, selRange);
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
- }
- }
-
- if (PDFMenu.Instance.Highlighting) {
- this.highlight(undefined, "goldenrod");
- }
- else {
- PDFMenu.Instance.StartDrag = this.startDrag;
- PDFMenu.Instance.Highlight = this.highlight;
- }
- document.removeEventListener("pointermove", this.onSelectStart);
- document.removeEventListener("pointerup", this.onSelectEnd);
- }
-
- @action
- createTextAnnotation = (sel: Selection, selRange: Range) => {
- if (this._textLayer.current) {
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- let clientRects = selRange.getClientRects();
- for (let i = 0; i < clientRects.length; i++) {
- let rect = clientRects.item(i);
- if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {
- let annoBox = document.createElement("div");
- annoBox.className = "pdfPage-annotationBox";
- // transforms the positions from screen onto the pdf div
- annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();
- annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString();
- annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString();
- annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString();
- this.props.createAnnotation(annoBox, this.props.page);
- }
- }
- }
- let text = selRange.extractContents().textContent;
- text && this.props.setSelectionText(text);
-
- // clear selection
- if (sel.empty) { // Chrome
- sel.empty();
- } else if (sel.removeAllRanges) { // Firefox
- sel.removeAllRanges();
- }
- }
-
- doubleClick = (e: React.MouseEvent) => {
- if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- // do something to select the paragraph ideally
- }
- }
-
- render() {
- return (
- <div className={"pdfPage-cont"} onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} style={{ "width": this._width, "height": this._height }}>
- <canvas className="PdfPage-canvasContainer" ref={this._canvas} />
- <div className="pdfPage-dragAnnotationBox" ref={this._marquee}
- style={{
- left: `${this._marqueeX}px`, top: `${this._marqueeY}px`,
- width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`,
- border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}`
- }}>
- </div>
- <div className="pdfPage-textlayer" ref={this._textLayer} />
- </div>);
- }
-} \ 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..daf000dc7
--- /dev/null
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -0,0 +1,225 @@
+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 { 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 { Transform } from "../../util/Transform";
+import { CollectionViewType } from '../collections/CollectionBaseView';
+import { DocumentView } from "../nodes/DocumentView";
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import "./PresElementBox.scss";
+import React = require("react");
+import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
+
+
+library.add(faArrowUp);
+library.add(fileSolid);
+library.add(faLocationArrow);
+library.add(fileRegular as any);
+library.add(faSearch);
+library.add(faArrowDown);
+/**
+ * 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 React.Component<FieldViewProps> {
+
+ public static LayoutString() { return FieldView.LayoutString(PresElementBox); }
+
+ @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); }
+ @computed get presentationDoc() { return this.props.Document.presBox as Doc; }
+ @computed get presentationFieldKey() { return StrCast(this.props.Document.presBoxKey); }
+ @computed get currentIndex() { return NumCast(this.presentationDoc.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 embedOpen() { return BoolCast(this.props.Document.embedOpen); }
+
+ set embedOpen(value: boolean) { this.props.Document.embedOpen = value; }
+ 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; }
+
+ /**
+ * 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.myIndex >= this.currentIndex) {
+ (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+ }
+ } else {
+ if (this.presentationDoc.presStatus && this.myIndex > this.currentIndex) {
+ (this.props.Document.presentationTargetDoc as Doc).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.myIndex <= this.currentIndex) {
+ (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+ }
+ } else {
+ if (this.fadeButton) this.fadeButton = false;
+ if (this.presentationDoc.presStatus && this.myIndex < this.currentIndex) {
+ (this.props.Document.presentationTargetDoc as Doc).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.myIndex <= this.currentIndex) {
+ (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+ }
+ } else {
+ this.hideAfterButton = false;
+ if (this.presentationDoc.presStatus && (this.myIndex < this.currentIndex)) {
+ (this.props.Document.presentationTargetDoc as Doc).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.myIndex) {
+ 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.showButton = !this.showButton;
+ if (!this.showButton) {
+ this.props.Document.viewScale = 1;
+ } else {
+ this.navButton = false;
+ if (this.currentIndex === this.myIndex) {
+ 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.embedOpen || !(this.props.Document.presentationTargetDoc instanceof Doc)) {
+ return (null);
+ }
+
+ let propDocWidth = NumCast(this.props.Document.nativeWidth);
+ let propDocHeight = NumCast(this.props.Document.nativeHeight);
+ let scale = () => 175 / NumCast(this.props.Document.nativeWidth, 175);
+ return (
+ <div className="presElementBox-embedded" style={{
+ height: propDocHeight === 0 ? NumCast(this.props.Document.height) - NumCast(this.props.Document.collapsedHeight) : propDocHeight * scale(),
+ width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
+ }}>
+ <CollectionSchemaPreview
+ fitToBox={StrCast(this.props.Document.presentationTargetDoc.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.props.Document.presentationTargetDoc}
+ 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 p = this.props;
+
+ let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
+ let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : "");
+ let pbi = "presElementBox-interaction";
+ return (
+ <div className={className} key={p.Document[Id] + this.myIndex}
+ style={{ outlineWidth: Doc.IsBrushed(p.Document.presentationTargetDoc as Doc) ? `1px` : "0px", }}
+ onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
+ {treecontainer ? (null) : <>
+ <strong className="presElementBox-name">
+ {`${this.myIndex + 1}. ${p.Document.title}`}
+ </strong>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
+ <br />
+ </>
+ }
+ <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button>
+ <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.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 83413814f..000000000
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ /dev/null
@@ -1,425 +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}
- ScreenToLocalTransform={Transform.Identity}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- renderDepth={1}
- PanelWidth={() => 350}
- PanelHeight={() => 90}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- selectOnLoad={false}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- ContainingCollectionView={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 4de8da587..000000000
--- a/src/client/views/presentationview/PresentationModeMenu.tsx
+++ /dev/null
@@ -1,100 +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 _right: number = 0;
- @observable private _opacity: number = 1;
- @observable private _transition: string = "opacity 0.5s";
- @observable private _transitionDelay: string = "";
-
-
- 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._right -= e.movementX;
- this._top += e.movementY;
-
- 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);
-
- 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={{ right: this._right, 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 c13d1d276..da733d64b 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -195,11 +195,9 @@ export class FilterBox extends React.Component {
collections.push(element.props.Document);
}
}
- //gets the selected doc's containing view
- let containingView = element.props.ContainingCollectionView;
//makes sure collections aren't added more than once
- if (containingView && !collections.includes(containingView.props.Document)) {
- collections.push(containingView.props.Document);
+ if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) {
+ collections.push(element.props.ContainingCollectionDoc);
}
});
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 5ed33a596..0dd4d3dc5 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -34,6 +34,9 @@
&.searchBox-filter {
align-self: stretch;
+ }
+
+ &.searchBox-submit {
margin-left: 2px;
margin-right: 2px
}
@@ -45,6 +48,12 @@
}
}
+.searchBox-quickFilter {
+ width: 500px;
+ margin-left: 25px;
+ margin-top: 10px;
+}
+
.searchBox-results {
margin-right: 136px;
top: 300px;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 2ad69daca..be75a29e0 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,25 +1,25 @@
-import * as React from 'react';
-import { observer } from 'mobx-react';
-import { observable, action, runInAction, flow, computed } from 'mobx';
-import "./SearchBox.scss";
-import "./FilterBox.scss";
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { library } from '@fortawesome/fontawesome-svg-core';
-import { SetupDrag } from '../../util/DragManager';
-import { Docs } from '../../documents/Documents';
-import { NumCast, Cast } from '../../../new_fields/Types';
-import { Doc } from '../../../new_fields/Doc';
-import { SearchItem } from './SearchItem';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
import * as rp from 'request-promise';
+import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { SearchUtil } from '../../util/SearchUtil';
+import { Cast, NumCast } from '../../../new_fields/Types';
import { RouteStore } from '../../../server/RouteStore';
-import { FilterBox } from './FilterBox';
-import { ReadStream } from 'fs';
-import * as $ from 'jquery';
-import { MainView } from '../MainView';
import { Utils } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { SetupDrag } from '../../util/DragManager';
+import { SearchUtil } from '../../util/SearchUtil';
+import { MainView } from '../MainView';
+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);
@@ -29,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[] = [];
@@ -141,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: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}"`).join(" ")})` : "");
}
@@ -161,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]);
@@ -170,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);
}
});
});
@@ -222,7 +226,6 @@ export class SearchBox extends React.Component {
doc.width = size;
doc.height = size;
}
- doc.zoomBasis = 1;
x += 250;
if (x > 1000) {
x = 0;
@@ -234,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();
@@ -299,19 +302,21 @@ 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) {
- this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ 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]} lines={result[2]} highlighting={highlights} />;
this._isSearch[i] = "search";
}
}
else {
result = this._results[i];
if (result) {
- this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />;
+ 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]} lines={result[2]} highlighting={highlights} />;
this._isSearch[i] = "search";
}
}
@@ -338,12 +343,16 @@ 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-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
<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>
</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..62715c5eb 100644
--- a/src/client/views/search/SearchItem.scss
+++ b/src/client/views/search/SearchItem.scss
@@ -4,7 +4,6 @@
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
- height: 70px;
z-index: 0;
}
@@ -15,9 +14,12 @@
border-color: $intermediate-color;
border-bottom-style: solid;
padding: 10px;
- height: 70px;
+ min-height: 70px;
+ max-height: 150px;
+ height: auto;
z-index: 0;
display: inline-block;
+ overflow: auto;
.main-search-info {
display: flex;
@@ -26,6 +28,7 @@
.search-title-container {
width: 100%;
+ overflow: hidden;
.search-title {
text-transform: uppercase;
@@ -181,6 +184,12 @@
background: $lighter-alt-accent;
}
+.search-highlighting {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+}
+
.searchBox-instances {
float: left;
opacity: 1;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 41fc49c2e..a7822ed46 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";
@@ -28,8 +25,9 @@ import "./SelectorContextMenu.scss";
export interface SearchItemProps {
doc: Doc;
- query?: string;
+ query: string;
highlighting: string[];
+ lines: string[];
}
library.add(faCaretUp);
@@ -63,6 +61,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
runInAction(() => {
this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc }));
this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+
});
}
@@ -70,12 +69,12 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
return () => {
col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
- const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
col.panX = newPanX;
col.panY = newPanY;
}
- CollectionDockingView.Instance.AddRightSplit(col, undefined);
+ CollectionDockingView.AddRightSplit(col, undefined);
};
}
render() {
@@ -109,7 +108,7 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> {
unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc);
- getOnClick = (col: Doc) => () => CollectionDockingView.Instance.AddRightSplit(col, undefined);
+ getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col, undefined);
render() {
return (
@@ -127,68 +126,31 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> {
export class SearchItem extends React.Component<SearchItemProps> {
@observable _selected: boolean = false;
- private _previewDoc?: Doc;
onClick = () => {
// I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like
DocumentManager.Instance.jumpToDocument(this.props.doc, false);
- if (this.props.doc.data instanceof RichTextField) {
- this.highlightTextBox(this.props.doc);
- }
- // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined);
}
@observable _useIcons = true;
@observable _displayDim = 50;
- highlightTextBox = (doc: Doc) => {
- if (this.props.query) {
- const fieldkey = 'search_string';
- if (Object.keys(doc).indexOf(fieldkey) === -1) {
- doc.search_string = this.props.query;
- }
- else {
- doc.search_string = undefined;
- }
-
- }
- }
-
- fitToBox = () => {
- let bounds = Doc.ComputeContentBounds([this.props.doc]);
- return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim];
+ componentDidMount() {
+ this.props.doc.search_string = this.props.query;
+ this.props.doc.search_fields = this.props.highlighting.join(", ");
}
-
componentWillUnmount() {
- if (this._previewDoc) {
- DocServer.DeleteDocument(this._previewDoc[Id]);
- }
+ this.props.doc.search_string = undefined;
+ this.props.doc.search_fields = undefined;
}
-
//@computed
@action
public DocumentIcon() {
let layoutresult = StrCast(this.props.doc.type);
if (!this._useIcons) {
- let renderDoc = this.props.doc;
- //let box: number[] = [];
- if (layoutresult.indexOf(DocumentType.COL) !== -1) {
- renderDoc = Doc.MakeDelegate(renderDoc);
- let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => {
- var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
- let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
- return {
- x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
- r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- };
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
- let box = () => [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim];
- }
let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
let returnYDimension = () => this._displayDim;
- let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
- let newRenderDoc = Doc.MakeDelegate(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
- this._previewDoc = newRenderDoc;
+ let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
const docview = <div
onPointerDown={action(() => {
this._useIcons = !this._useIcons;
@@ -201,6 +163,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
Document={this.props.doc}
addDocument={returnFalse}
removeDocument={returnFalse}
+ ruleProvider={undefined}
ScreenToLocalTransform={Transform.Identity}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -209,25 +172,18 @@ export class SearchItem extends React.Component<SearchItemProps> {
PanelHeight={returnYDimension}
focus={emptyFunction}
backgroundColor={returnEmptyString}
- selectOnLoad={false}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
bringToFront={emptyFunction}
zoomToScale={emptyFunction}
getScale={returnOne}
ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
ContentScaling={scale}
/>
</div>;
- const data = renderDoc.data;
- if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data);
- newRenderDoc.preview = true;
- newRenderDoc.search_string = this.props.query;
return docview;
}
- if (this._previewDoc) {
- DocServer.DeleteDocument(this._previewDoc[Id]);
- }
let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf :
layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage :
layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote :
@@ -269,6 +225,12 @@ export class SearchItem extends React.Component<SearchItemProps> {
@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) {
@@ -279,9 +241,9 @@ export class SearchItem extends React.Component<SearchItemProps> {
Doc.BrushDoc(doc2);
}
} else {
- DocumentManager.Instance.getAllDocumentViews(this.props.doc).forEach(element =>
- Doc.BrushDoc(element.props.Document));
+ Doc.BrushDoc(this.props.doc);
}
+ e.stopPropagation();
}
unHighlightDoc = (e: React.PointerEvent) => {
@@ -294,8 +256,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
Doc.UnBrushDoc(doc2);
}
} else {
- DocumentManager.Instance.getAllDocumentViews(this.props.doc).
- forEach(element => Doc.UnBrushDoc(element.props.Document));
+ Doc.UnBrushDoc(this.props.doc);
}
}
@@ -315,7 +276,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc;
- DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc], []), e.clientX, e.clientY, {
+ DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
handlers: { dragComplete: emptyFunction },
hideSource: false,
});
@@ -326,13 +287,14 @@ 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 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"}`}>
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 4f4db13d2..fd6b47ff0 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -28,12 +28,8 @@ class Repl extends React.Component {
if (!script.compiled) {
this.executedCommands.push({ command: this.text, result: "Compile Error" });
} else {
- const result = script.run({ makeInterface });
- if (result.success) {
- this.executedCommands.push({ command: this.text, result: result.result });
- } else {
- this.executedCommands.push({ command: this.text, result: result.error.message || result.error });
- }
+ const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e }));
+ result.success && this.executedCommands.push({ command: this.text, result: result.result });
}
this.text = "";
}
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 79f87f4ac..3baedce4b 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -2,39 +2,12 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { DocServer } from '../client/DocServer';
import { Doc } from '../new_fields/Doc';
+import * as Pdfjs from "pdfjs-dist";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { Utils } from '../Utils';
+const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const protoId = "protoDoc";
const delegateId = "delegateDoc";
class Test extends React.Component {
- onCreateClick = () => {
- const proto = new Doc(protoId, true);
- const delegate = Doc.MakeDelegate(proto, delegateId);
- }
-
- onReadClick = async () => {
- console.log("reading");
- const docs = await DocServer.GetRefFields([delegateId, protoId]);
- console.log("done");
- console.log(docs);
- }
-
- onDeleteClick = () => {
- DocServer.DeleteDocuments([protoId, delegateId]);
- }
-
- render() {
- return (
- <div>
- <button onClick={this.onCreateClick}>Create Docs</button>
- <button onClick={this.onReadClick}>Read Docs</button>
- <button onClick={this.onDeleteClick}>Delete Docs</button>
- </div>
- );
- }
}
-
-DocServer.init(window.location.protocol, window.location.hostname, 4321, "test");
-ReactDOM.render(
- <Test />,
- document.getElementById('root')
-); \ No newline at end of file
diff --git a/src/extensions/ArrayExtensions.ts b/src/extensions/ArrayExtensions.ts
new file mode 100644
index 000000000..422a10dbc
--- /dev/null
+++ b/src/extensions/ArrayExtensions.ts
@@ -0,0 +1,13 @@
+function Assign() {
+
+ Array.prototype.lastElement = function <T>() {
+ if (!this.length) {
+ return undefined;
+ }
+ const last: T = this[this.length - 1];
+ return last;
+ };
+
+}
+
+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 1d529ac84..418863bcc 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,8 +1,8 @@
-import { observable, ObservableMap, runInAction } from "mobx";
+import { observable, ObservableMap, runInAction, action } from "mobx";
import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
-import { CompileScript, Scripting, scriptingGlobal } from "../client/util/Scripting";
+import { Scripting, scriptingGlobal } from "../client/util/Scripting";
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, Update } from "./FieldSymbols";
import { List } from "./List";
@@ -13,6 +13,7 @@ import { listSpec } from "./Schema";
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";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -62,6 +63,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[];
}
@@ -143,7 +148,7 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [WidthSym] = () => NumCast(this[SelfProxy].width); // bcz: is this the right way to access width/height? it didn't work with : this.width
+ public [WidthSym] = () => NumCast(this[SelfProxy].width);
public [HeightSym] = () => NumCast(this[SelfProxy].height);
[ToScriptString]() {
@@ -300,15 +305,15 @@ export namespace Doc {
export function AreProtosEqual(doc?: Doc, other?: Doc) {
if (!doc || !other) return false;
let r = (doc === other);
- let r2 = (doc.proto === other);
- let r3 = (other.proto === doc);
- let r4 = (doc.proto === other.proto && other.proto !== undefined);
+ let r2 = (Doc.GetProto(doc) === other);
+ let r3 = (Doc.GetProto(other) === doc);
+ let r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined);
return r || r2 || r3 || r4;
}
// gets the document's prototype or returns the document if it is a prototype
export function GetProto(doc: Doc) {
- return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc);
+ return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
}
export function GetDataDoc(doc: Doc): Doc {
let proto = Doc.GetProto(doc);
@@ -327,14 +332,31 @@ export namespace Doc {
return Array.from(results);
}
- 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 IndexOf(toFind: Doc, list: Doc[]) {
+ return 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(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 && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+ let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
if (pind !== -1) {
list.splice(pind, 1);
}
@@ -373,13 +395,13 @@ export namespace Doc {
}
//
- // Resolves a reference to a field by returning 'doc' if field extension is specified,
+ // 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 resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) {
+ export function fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
}
@@ -396,26 +418,28 @@ export namespace Doc {
return docExtensionForField;
}
- export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
if (docExtensionForField === undefined) {
- setTimeout(() => {
- CreateDocumentExtensionForField(doc, fieldKey);
- }, 0);
+ 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;
+ return doc;
+ }
export function MakeAlias(doc: Doc) {
let alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
- let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
- let script = `return renameAlias(self, ${aliasNumber})`;
- //let script = "StrCast(self.title).replace(/\\([0-9]*\\)/, \"\") + `(${n})`";
- let compiled = CompileScript(script, { params: { this: "Doc" }, capturedVariables: { self: doc }, typecheck: false });
- if (compiled.compiled) {
- alias.title = new ComputedField(compiled);
+ if (alias.layout instanceof Doc) {
+ alias.layout = Doc.MakeAlias(alias.layout);
}
+ let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
+ alias.title = ComputedField.MakeFunction(`renameAlias(this, ${aliasNumber})`);
return alias;
}
@@ -466,19 +490,42 @@ export namespace Doc {
let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
- let fieldExtensionDoc = Doc.resolvedFieldDataDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
+ 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);
return { layout: layoutDoc, data: resolvedDataDoc };
}
- export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
- const copy = new Doc;
+ export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
Object.keys(doc).forEach(key => {
const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === "proto" && copyProto) {
- if (field instanceof Doc) {
- copy[key] = Doc.MakeCopy(field);
+ if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) {
+ overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto);
+ }
+ } else {
+ if (field instanceof RefField) {
+ overwrite[key] = field;
+ } else if (field instanceof ObjectField) {
+ overwrite[key] = ObjectField.MakeCopy(field);
+ } else if (field instanceof Promise) {
+ debugger; //This shouldn't happend...
+ } else {
+ overwrite[key] = field;
+ }
+ }
+ });
+
+ return overwrite;
+ }
+
+ export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
+ const copy = new Doc(copyProtoId, true);
+ Object.keys(doc).forEach(key => {
+ const field = ProxyField.WithoutProxy(() => doc[key]);
+ if (key === "proto" && copyProto) {
+ if (doc[key] instanceof Doc) {
+ copy[key] = Doc.MakeCopy(doc[key]!, false);
}
} else {
if (field instanceof RefField) {
@@ -510,21 +557,9 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
- if (!templateDoc) return undefined;
- let datadoc = new Doc();
- let otherdoc = Doc.MakeDelegate(datadoc);
- otherdoc.width = templateDoc[WidthSym]();
- otherdoc.height = templateDoc[HeightSym]();
- otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")";
- otherdoc.layout = Doc.MakeDelegate(templateDoc);
- otherdoc.miniLayout = StrCast(templateDoc.miniLayout);
- otherdoc.detailedLayout = otherdoc.layout;
- otherdoc.type = DocumentType.TEMPLATE;
- !templateDoc.nativeWidth && (otherdoc.nativeWidth = 0);
- !templateDoc.nativeHeight && (otherdoc.nativeHeight = 0);
- return otherdoc;
- }
- export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) {
+ return !templateDoc ? undefined : ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+ }
+ export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
if (!templateDoc) {
target.layout = undefined;
target.nativeWidth = undefined;
@@ -533,80 +568,90 @@ export namespace Doc {
target.type = undefined;
return;
}
- let temp = Doc.MakeDelegate(templateDoc);
- target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined;
- target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined;
- !templateDoc.nativeWidth && (target.nativeWidth = 0);
- !templateDoc.nativeHeight && (target.nativeHeight = 0);
+
+ let layoutCustom = Doc.MakeTitled("layoutCustom");
+ let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
+
+ titleTarget && (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.type = DocumentType.TEMPLATE;
- if (targetData && targetData.layout === target) {
- targetData.layout = temp;
- targetData.miniLayout = StrCast(templateDoc.miniLayout);
- targetData.detailedLayout = targetData.layout;
- } else {
- target.layout = temp;
- target.miniLayout = StrCast(templateDoc.miniLayout);
- target.detailedLayout = target.layout;
- }
+ target.layout = layoutCustomLayout;
+ target.backgroundLayout = layoutCustomLayout.backgroundLayout;
+
+ target.layoutNative = Cast(templateDoc.layoutNative, Doc) as Doc;
+ target.layoutCustom = layoutCustom;
+ return target;
}
- export function MakeTemplate(fieldTemplate: Doc, metaKey: string, templateDataDoc: Doc) {
+ export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) {
// 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);
}
- let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
if (backgroundLayout) {
- backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
}
- let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
- layoutDelegate.layout = layout;
-
- fieldTemplate.templateField = metaKey;
- fieldTemplate.title = metaKey;
+ fieldTemplate.templateField = metadataFieldName;
+ fieldTemplate.title = metadataFieldName;
fieldTemplate.isTemplate = true;
- fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
fieldTemplate.backgroundLayout = backgroundLayout;
/* 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.
*/
- fieldTemplate.ignoreAspect = BoolCast(fieldTemplate.ignoreAspect);
+ fieldTemplate.ignoreAspect = fieldTemplate.ignoreAspect === undefined ? undefined : BoolCast(fieldTemplate.ignoreAspect);
fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
- fieldTemplate.showTitle = "title";
- setTimeout(() => fieldTemplate.proto = templateDataDoc);
+ fieldTemplate.panX = 0;
+ fieldTemplate.panY = 0;
+ fieldTemplate.scale = 1;
+ fieldTemplate.showTitle = suppressTitle ? undefined : "title";
+ let data = fieldTemplate.data;
+ 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;
+ layoutDelegate.layout = layout;
+ fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+ if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
+ fieldTemplate.proto = templateDataDoc;
+ }), 0);
+ }
+
+ export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+ 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 });
+ }
+
+ export function isBrushedHighlightedDegree(doc: Doc) {
+ if (Doc.IsHighlighted(doc)) {
+ return 6;
+ }
+ else {
+ return Doc.IsBrushedDegree(doc);
+ }
}
- export function ToggleDetailLayout(d: Doc) {
- runInAction(async () => {
- let miniLayout = await PromiseValue(d.miniLayout);
- let detailLayout = await PromiseValue(d.detailedLayout);
- d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);
- if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined;
- });
- }
- export function UseDetailLayout(d: Doc) {
- runInAction(async () => {
- let detailLayout = await d.detailedLayout;
- if (detailLayout) {
- d.layout = detailLayout;
- d.nativeWidth = d.nativeHeight = undefined;
- if (detailLayout instanceof Doc) {
- let delegDetailLayout = Doc.MakeDelegate(detailLayout);
- d.layout = delegDetailLayout;
- delegDetailLayout.layout = await delegDetailLayout.detailedLayout;
- }
- }
- });
+ export class DocBrush {
+ @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
}
-
+ const brushManager = new DocBrush();
export class DocData {
@observable _user_doc: Doc = undefined!;
@@ -616,24 +661,73 @@ export namespace Doc {
export function UserDoc(): Doc { return manager._user_doc; }
export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
export function IsBrushed(doc: Doc) {
- return manager.BrushedDoc.has(doc) || manager.BrushedDoc.has(Doc.GetDataDoc(doc));
+ return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
}
export function IsBrushedDegree(doc: Doc) {
- return manager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 2 : manager.BrushedDoc.has(doc) ? 1 : 0;
+ return brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 2 : brushManager.BrushedDoc.has(doc) ? 1 : 0;
}
export function BrushDoc(doc: Doc) {
- manager.BrushedDoc.set(doc, true);
- manager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+ brushManager.BrushedDoc.set(doc, true);
+ brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+ return doc;
}
export function UnBrushDoc(doc: Doc) {
- manager.BrushedDoc.delete(doc);
- manager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ brushManager.BrushedDoc.delete(doc);
+ brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ return doc;
+ }
+
+ 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 {
+ @observable HighlightedDoc: Map<Doc, boolean> = new Map();
+ }
+ const highlightManager = new HighlightBrush();
+ export function IsHighlighted(doc: Doc) {
+ let IsHighlighted = highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
+ return IsHighlighted;
+ }
+ export function HighlightDoc(doc: Doc) {
+ runInAction(() => {
+ highlightManager.HighlightedDoc.set(doc, true);
+ highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true);
+ });
+ }
+ export function UnHighlightDoc(doc: Doc) {
+ runInAction(() => {
+ highlightManager.HighlightedDoc.set(doc, false);
+ highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), false);
+ });
+ }
+ export function UnhighlightAll() {
+ let mapEntries = highlightManager.HighlightedDoc.keys();
+ let docEntry: IteratorResult<Doc>;
+ while (!(docEntry = mapEntries.next()).done) {
+ let targetDoc = docEntry.value;
+ targetDoc && Doc.UnHighlightDoc(targetDoc);
+ }
+
}
export function UnBrushAllDocs() {
manager.BrushedDoc.clear();
}
}
-Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`; });
+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 getAlias(doc: any) { return Doc.MakeAlias(doc); });
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))); }); \ No newline at end of file
+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
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 8f64c1c2e..e381d0218 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -16,7 +16,7 @@ export interface StrokeData {
color: string;
width: string;
tool: InkTool;
- page: number;
+ displayTimecode: number;
}
export type InkData = Map<string, StrokeData>;
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 1b52e6f82..d2f76c969 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 {
@@ -28,48 +23,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/ScriptField.ts b/src/new_fields/ScriptField.ts
index 83fb52d07..cdc9871a8 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -1,5 +1,5 @@
import { ObjectField } from "./ObjectField";
-import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting";
+import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions } from "../client/util/Scripting";
import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
@@ -101,19 +101,39 @@ export class ScriptField extends ObjectField {
[ToScriptString]() {
return "script field";
}
+ public static CompileScript(script: string, params: object = {}, addReturn = false) {
+ let compiled = CompileScript(script, {
+ params: { this: Doc.name, ...params },
+ typecheck: false,
+ editable: true,
+ addReturn: addReturn
+ });
+ return compiled;
+ }
+ public static MakeFunction(script: string, params: object = {}) {
+ let compiled = ScriptField.CompileScript(script, params, true);
+ return compiled.compiled ? new ScriptField(compiled) : undefined;
+ }
+
+ public static MakeScript(script: string, params: object = {}) {
+ let compiled = ScriptField.CompileScript(script, params, false);
+ return compiled.compiled ? new ScriptField(compiled) : undefined;
+ }
}
@scriptingGlobal
@Deserializable("computed", deserializeScript)
export class ComputedField extends ScriptField {
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
- value = computedFn((doc: Doc) => {
- const val = this.script.run({ this: doc });
- if (val.success) {
- return val.result;
- }
- return undefined;
- });
+ value = computedFn((doc: Doc) => this.script.run({ this: doc }, console.log).result);
+ public static MakeScript(script: string, params: object = {}, ) {
+ let compiled = ScriptField.CompileScript(script, params, false);
+ return compiled.compiled ? new ComputedField(compiled) : undefined;
+ }
+ public static MakeFunction(script: string, params: object = {}) {
+ let compiled = ScriptField.CompileScript(script, params, true);
+ return compiled.compiled ? new ComputedField(compiled) : undefined;
+ }
}
export namespace ComputedField {
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index 807216ef1..a9256073b 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -88,7 +88,6 @@ def write_collection(parse_results, display_fields, storage_key, viewType=2):
"height": 600,
"panX": 0,
"panY": 0,
- "zoomBasis": 1,
"zIndex": 2,
"libraryBrush": False,
"viewType": viewType
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/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 8785cd974..963c7736a 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,9 @@ export namespace GoogleApiServerUtils {
'presentations.readonly',
'drive',
'drive.file',
+ 'photoslibrary',
+ 'photoslibrary.appendonly',
+ 'photoslibrary.sharing'
];
export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
@@ -29,102 +35,146 @@ 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 const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<TokenResult> => {
+ const oAuth2Client = await RetrieveOAuthClient(information);
+ return new Promise<TokenResult>((resolve, reject) => {
+ oAuth2Client.getToken(authenticationCode, async (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token);
+ resolve({ token, client: oAuth2Client });
+ });
+ });
+ };
+ export const RetrieveCredentials = (information: CredentialInformation) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ readFile(information.credentialsPath, async (err, credentials) => {
+ if (err) {
+ reject(err);
+ return console.log('Error loading client secret file:', err);
+ }
+ 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 };
/**
* Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
* @param {Object} credentials The authorization client credentials.
*/
- export function authorize(credentials: any, token_path: string): Promise<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<OAuth2Client>((resolve, reject) => {
- readFile(token_path, (err, token) => {
- // Check if we have previously stored a token.
- if (err) {
- return getNewToken(oAuth2Client, token_path).then(resolve, reject);
+ 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);
}
- oAuth2Client.setCredentials(parseBuffer(token));
- resolve(oAuth2Client);
+ // Authentication successful!
+ oAuth2Client.setCredentials(token!);
+ resolve({ token: token!, 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.
- */
- 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);
- });
+ 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
new file mode 100644
index 000000000..4a67e57cc
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -0,0 +1,85 @@
+import request = require('request-promise');
+import { GoogleApiServerUtils } from './GoogleApiServerUtils';
+import * as path from 'path';
+import { MediaItemCreationResult } from './SharedTypes';
+import { NewMediaItem } from "../../index";
+import { BatchedArray, TimeUnit } from 'array-batcher';
+import { DashUploadUtils } from '../../DashUploadUtils';
+
+export namespace GooglePhotosUploadUtils {
+
+ export interface Paths {
+ uploadDirectory: string;
+ credentialsPath: string;
+ tokenPath: string;
+ }
+
+ export interface MediaInput {
+ url: string;
+ description: string;
+ }
+
+ const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
+ const headers = (type: string) => ({
+ 'Content-Type': `application/${type}`,
+ 'Authorization': Bearer,
+ });
+
+ let Bearer: string;
+
+ 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',
+ headers: {
+ ...headers('octet-stream'),
+ 'X-Goog-Upload-File-Name': path.basename(url),
+ 'X-Goog-Upload-Protocol': 'raw'
+ },
+ uri: prepend('uploads'),
+ body
+ };
+ return new Promise<any>((resolve, reject) => request(parameters, (error, _response, body) => {
+ if (error) {
+ console.log(error);
+ return reject(error);
+ }
+ resolve(body);
+ }));
+ };
+
+ export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise<MediaItemCreationResult> => {
+ const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: NewMediaItem[]) => {
+ const parameters = {
+ method: 'POST',
+ headers: headers('json'),
+ uri: prepend('mediaItems:batchCreate'),
+ body: { newMediaItems: batch } as any,
+ json: true
+ };
+ album && (parameters.body.albumId = album.id);
+ return (await new Promise<MediaItemCreationResult>((resolve, reject) => {
+ request(parameters, (error, _response, body) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(body);
+ }
+ });
+ })).newMediaItemResults;
+ }
+ );
+ return { newMediaItemResults };
+ };
+
+} \ 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 e87b93ddf..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';
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index f7ce24967..0fbfbf2f3 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -2,12 +2,11 @@ import { action, computed, observable, 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 { Doc } from "../../../new_fields/Doc";
+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";
@@ -24,6 +23,9 @@ export class CurrentUserUtils {
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
+ @observable public static GuestTarget: Doc | undefined;
+ @observable public static GuestWorkspace: Doc | undefined;
+
private static createUserDocument(id: string): Doc {
let doc = new Doc(id, true);
doc.viewType = CollectionViewType.Tree;
@@ -36,46 +38,99 @@ export class CurrentUserUtils {
doc.xMargin = 5;
doc.yMargin = 5;
doc.boxShadow = "0 0";
- doc.excludeFromLibrary = true;
doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
return doc;
}
static updateUserDocument(doc: Doc) {
+
+ // setup workspaces library item
if (doc.workspaces === undefined) {
- const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
- workspaces.excludeFromLibrary = true;
- workspaces.workspaceLibrary = true;
+ const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces".toUpperCase(), height: 100 });
workspaces.boxShadow = "0 0";
doc.workspaces = workspaces;
}
- PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => workspaces && (workspaces.preventTreeViewOpen = true));
+ PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => {
+ if (workspaces) {
+ workspaces.backgroundColor = "#eeeeee";
+ workspaces.preventTreeViewOpen = true;
+ workspaces.forceActive = true;
+ workspaces.lockedPosition = true;
+ if (StrCast(workspaces.title) === "Workspaces") {
+ workspaces.title = "WORKSPACES";
+ }
+ }
+ });
+
+ // 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));
+
+ // setup Recently Closed library item
if (doc.recentlyClosed === undefined) {
- const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
- recentlyClosed.excludeFromLibrary = true;
+ const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed".toUpperCase(), height: 75 });
recentlyClosed.boxShadow = "0 0";
doc.recentlyClosed = recentlyClosed;
}
- PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => recent && (recent.preventTreeViewOpen = true));
+ PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => {
+ if (recent) {
+ recent.backgroundColor = "#eeeeee";
+ recent.preventTreeViewOpen = true;
+ recent.forceActive = true;
+ recent.lockedPosition = true;
+ if (StrCast(recent.title) === "Recently Closed") {
+ recent.title = "RECENTLY CLOSED";
+ }
+ }
+ });
+
+
if (doc.curPresentation === undefined) {
const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
- curPresentation.excludeFromLibrary = true;
curPresentation.boxShadow = "0 0";
doc.curPresentation = curPresentation;
}
+
if (doc.sidebar === undefined) {
const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
- sidebar.excludeFromLibrary = true;
+ 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;
}
- StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
+ PromiseValue(Cast(doc.sidebar, Doc)).then(sidebar => {
+ if (sidebar) {
+ sidebar.backgroundColor = "lightgrey";
+ }
+ });
+
+ if (doc.overlays === undefined) {
+ const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
+ Doc.GetProto(overlays).backgroundColor = "#aca3a6";
+ doc.overlays = overlays;
+ }
+
+ 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" })));
+ }
+
+ StrCast(doc.title).indexOf("@") !== -1 && (doc.title = (StrCast(doc.title).split("@")[0] + "'s Library").toUpperCase());
+ StrCast(doc.title).indexOf("'s Library") !== -1 && (doc.title = StrCast(doc.title).toUpperCase());
+ doc.backgroundColor = "#eeeeee";
doc.width = 100;
doc.preventTreeViewOpen = true;
+ doc.forceActive = true;
+ doc.lockedPosition = true;
}
public static loadCurrentUser() {
@@ -93,7 +148,7 @@ export class CurrentUserUtils {
this.curr_id = id;
Doc.CurrentUserEmail = email;
await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
- if (id) {
+ if (id && id !== "guest") {
return DocServer.GetRefField(id).then(async field => {
if (field instanceof Doc) {
await this.updateUserDocument(field);
@@ -106,17 +161,17 @@ export class CurrentUserUtils {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
});
- try {
- const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
- NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
- await Gateway.Instance.ClearCatalog();
- const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
- let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
- let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
- // if (catprom) await Promise.all(catprom);
- } catch (e) {
+ // try {
+ // const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
+ // NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
+ // await Gateway.Instance.ClearCatalog();
+ // const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
+ // let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
+ // let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
+ // // if (catprom) await Promise.all(catprom);
+ // } catch (e) {
- }
+ // }
}
/* Northstar catalog ... really just for testing so this should eventually go away */
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..990441d5a 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -1,209 +1,294 @@
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';
-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, token: any) => {
+ return Instance.insert({ userId, canAccess: [], ...token }, 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 de46ebf71..62938b9c7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -29,26 +29,32 @@ 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';
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
@@ -115,7 +121,9 @@ function addSecureRoute(method: Method,
...subscribers: string[]
) {
let abstracted = (req: express.Request, res: express.Response) => {
- if (req.user) {
+ let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true";
+ sharing = sharing && req.originalUrl.startsWith("/doc/");
+ if (req.user || sharing) {
handler(req.user as any, res, req);
} else {
req.session!.target = req.originalUrl;
@@ -157,6 +165,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 +211,23 @@ const solrURL = "http://localhost:8983/solr/#/dash";
// GETTERS
+app.get("/textsearch", async (req, res) => {
+ let q = req.query.q;
+ console.log("TEXTSEARCH " + 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 +321,76 @@ 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>;
+
+app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (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 +522,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 +536,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);
}
});
});
@@ -503,21 +605,20 @@ addSecureRoute(
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
},
undefined,
- RouteStore.home,
- RouteStore.openDocumentWithId
+ RouteStore.home, RouteStore.openDocumentWithId
);
addSecureRoute(
Method.GET,
- (user, res) => res.send(user.userDocumentId || ""),
- undefined,
+ (user, res) => res.send(user.userDocumentId),
+ (res) => res.send(undefined),
RouteStore.getUserDocumentId,
);
addSecureRoute(
Method.GET,
- (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })),
- undefined,
+ (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); },
+ (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })),
RouteStore.getCurrUser
);
@@ -556,54 +657,65 @@ class NodeCanvasFactory {
}
const pngTypes = [".png", ".PNG"];
-const pdfTypes = [".pdf", ".PDF"];
const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-const uploadDir = __dirname + "/public/files/";
+const uploadDirectory = __dirname + "/public/files/";
+const pdfDirectory = uploadDirectory + "text";
+DashUploadUtils.createIfNotExists(pdfDirectory);
+
+interface ImageFileResponse {
+ name: string;
+ path: string;
+ type: string;
+ exif: Opt<DashUploadUtils.EnrichedExifData>;
+}
+
// SETTERS
app.post(
RouteStore.upload,
(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 {
+ 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);
});
}
);
+app.post(RouteStore.inspectImage, async (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.POST,
(user, res, req) => {
@@ -613,7 +725,7 @@ addSecureRoute(
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,7 +746,7 @@ 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);
@@ -681,21 +793,29 @@ app.use(RouteStore.corsProxy, (req, res) => {
}).pipe(res);
});
-app.get(RouteStore.delete, (req, res) => {
- if (release) {
- res.send("no");
- return;
- }
- deleteFields().then(() => res.redirect(RouteStore.home));
-});
+addSecureRoute(
+ Method.GET,
+ (user, res, req) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ deleteFields().then(() => res.redirect(RouteStore.home));
+ },
+ undefined,
+ RouteStore.delete
+);
-app.get(RouteStore.deleteAll, (req, res) => {
- if (release) {
- res.send("no");
- return;
- }
- deleteAll().then(() => res.redirect(RouteStore.home));
-});
+addSecureRoute(
+ Method.GET,
+ (_user, res, _req) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ deleteAll().then(() => res.redirect(RouteStore.home));
+ },
+ undefined,
+ RouteStore.deleteAll
+);
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -801,8 +921,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)],
@@ -811,10 +930,10 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU
]);
app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
- let sector = req.params.sector;
- let action = req.params.action;
- GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector as any], { credentials, token }).then(endpoint => {
- let handler = EndpointHandlerMap.get(action as any);
+ 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], { credentialsPath, userId: req.headers.userId as string }).then(endpoint => {
+ let handler = EndpointHandlerMap.get(action);
if (endpoint && handler) {
let execute = handler(endpoint, req.body).then(
response => res.send(response.data),
@@ -827,6 +946,151 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
+app.get(RouteStore.readGoogleAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ 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));
+});
+
+app.post(RouteStore.writeGoogleAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ const information = { credentialsPath, userId };
+ const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode);
+ res.send(token.access_token);
+});
+
+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;
+ };
+}
+
+app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
+ const { media } = req.body;
+ const userId = req.header("userId");
+
+ if (!userId) {
+ return _error(res, userIdError);
+ }
+
+ await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
+
+ let failed: number[] = [];
+
+ const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
+ const newMediaItems: NewMediaItem[] = [];
+ for (let index = 0; index < batch.length; index++) {
+ const element = batch[index];
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
+ if (!uploadToken) {
+ failed.push(index);
+ } else {
+ newMediaItems.push({
+ description: element.description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
+ }
+ return newMediaItems;
+ }
+ );
+
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+ }
+
+ GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
+ result => _success(res, { results: result.newMediaItemResults, 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